考虑一个场景,我构造了一个代表大整数的类 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 volatile("" :: "r"(value))
是 volatile
,因此无法优化掉,并且要求该值存在于寄存器中。但编译为零 asm 指令。
OTOH,这仍然允许将
a+b
计算提升到循环之外,因此您还需要让编译器忘记每次迭代中 a
和 b
的值,例如用 asm("" : "+g"(a), "+g"(b))
告诉编译器这两个变量都是 asm 语句修改的读/写操作数。这可能与 const
不兼容。也许对于只适合内存的 BigInt,您可以获取指向局部变量的指针并使用 "memory"
破坏器使编译器假设数据已被修改,即使您只有 const
引用。
您必须检查汇编以确保没有引入额外的存储/重新加载。 (如何从 GCC/clang 汇编输出中消除“噪音”?)。
"memory"
破坏者不会影响地址尚未被获取的本地变量,但是将地址作为输入操作数传递给 asm
语句应该使其尊重它们。
各种
DoNotOptimize
实现都以这种方式使用 GNU C 内联汇编 #ifdef __GNUC__
。像 nanobench 的 doNotOptimizeAway
和 Google::Benchmark 一样,请参阅相关问答:(Google Benchmark Frameworks DoNotOptimize / 以及另一个回复:其中的 "memory"
clobber )。 ISO C++ 没有可移植的方法来做到这一点,因此例如使用 MSVC,它们可能会将值存储到 volatile int sink
虚拟变量。
但是在循环携带的依赖链中引入存储/重新加载可能会像调试构建一样造成巨大的减慢(与 L1d 缓存可以吸收的重复存储相同位置不同),因此对于
"+g"(var)
约束没有良好的可移植等效项让编译器假设该值已被修改,而不实际运行任何指令。
"g"
让编译器可以选择内存或寄存器。但 clang 可能总是更喜欢记忆,所以 "+r,m"
可以解决这个问题。