提问人:lei hu 提问时间:9/15/2023 最后编辑:Peter Cordeslei hu 更新时间:9/15/2023 访问量:79
如何防止一段无副作用的代码被优化掉?
How to prevent a segment of Side Effect-Free code from being optimized away?
问:
考虑一个场景,我构造了一个表示大整数的类 T。此类 T 具有加法运算符函数。
class T {
public:
T operator+(const T &other) const
}
为了测试它的性能,我想反复 执行此函数足够次数,记录经过的总时间,然后计算平均时间。伪代码如下:
T a, b;
Record the starting timestamp.
for (int i = 0; i < 100000; i++) {
a + b;
}
Record the ending timestamp.
Calculate the average time.
但是,由于 a+b 运算的结果没有被保存,并且 operator+ 函数也没有以任何方式修改 a 或 b,所以 a + b;是无副作用的代码,将由编译器进行优化。一个可行的解决方案是改变
a + b;
自
T t = a + b;
store t somewhere
但是,这会引入对复制构造函数的调用,并增加存储 t 的开销,从而降低计时测量的准确性。有没有办法告诉编译器不要优化 a+b;?
答:
创建一个需要结果的副作用,例如 GNU C 无法优化,并且要求该值存在于寄存器中。但编译为零 asm 指令。asm volatile("" :: "r"(value))
volatile
OTOH,这仍然允许将计算提升到循环之外,因此您还需要让编译器忘记每次迭代的值,例如,告诉编译器两个变量都是 asm 语句修改的读/写操作数。这可能与 不兼容。也许对于只适合内存的 BigInt,您可以获取指向局部变量的指针,并使用 clobber 使编译器假设数据已被修改,即使您只有引用。a+b
a
b
asm("" : "+g"(a), "+g"(b))
const
"memory"
const
您必须检查 asm 以确保没有引入额外的存储/重新加载。(如何消除 GCC/clang 程序集输出中的“噪音”? clobber 不会影响未获取地址的本地 var,但将地址作为输入操作数传递给语句应该使其尊重它们。"memory"
asm
各种实现都以这种方式使用 GNU C 内联 asm。与 nanobench 的 doNotOptimizeAway
和 Google::Benchmark 一样,请参阅相关的 Q&As:(Google Benchmark Frameworks DoNotOptimize / 和另一个 re:其中的“内存”
clobber)。ISO C++ 没有可移植的方法来执行此操作,因此例如,使用 MSVC,它们可能会将值存储到虚拟变量。DoNotOptimize
#ifdef __GNUC__
volatile int sink
但是,在循环携带的依赖链中引入存储/重新加载可能会像调试构建一样速度减慢(不像只是重复存储 L1d 缓存可以吸收的相同位置),因此约束没有很好的可移植等价物来使编译器假设值已被修改而没有实际运行任何指令。"+g"(var)
"g"
允许编译器选择内存或寄存器。但是 clang 可能总是更喜欢内存,所以可以解决这个问题。"+r,m"
我能想到的最简单的方法是通过编译器标志将优化级别设置为零-O0
设置为零时,需要 10180 个时间单位。-O0
设置为 3 时,只需 90 个时间单位。-O3
您还可以检查生成的汇编代码。
评论