C++函数性能分析时如何防止指令重排?有无标准方案?
编译器真的会重排你的计时代码吗?
先明确:如果那段被测代码完全没有任何副作用(比如纯计算、不读写全局变量/IO),理论上编译器确实有可能把timeStart的调用挪到被测代码之后。虽然std::chrono::system_clock::now()本身是有副作用的(读取系统时钟),但编译器要是能判定被测代码不会影响时钟结果,且timeStart只用来算时间差,在O3这种高优化级别下,不排除出现你担心的重排——实际中这种情况不多,但严谨做性能分析的话,必须防一手。
怎么防止?不用全内存屏障这么重的东西
全内存屏障是给多线程内存可见性用的,单线程性能分析完全没必要,代价太高。C++标准里有更轻量的方式约束编译器优化:
1. 用std::atomic_signal_fence(跨平台轻量屏障)
这个函数只会限制编译器的指令重排,不会生成硬件层面的内存屏障指令,几乎没性能开销。具体插的位置:
#include <atomic> #include <chrono> #include <iostream> void foo() { const auto timeStart = std::chrono::system_clock::now(); // 拦着编译器把被测代码挪到timeStart前面 std::atomic_signal_fence(std::memory_order_seq_cst); // 要分析性能的代码..... // 拦着编译器把被测代码挪到timeEnd后面 std::atomic_signal_fence(std::memory_order_seq_cst); const auto timeEnd = std::chrono::system_clock::now(); std::cout << "耗时: " << (timeEnd - timeStart).count() << "\n"; }
memory_order_seq_cst是最严格的编译器约束,确保前后的代码不会跨过这个屏障重排。
2. 用std::atomic强制原子操作
把计时点声明为原子变量,利用原子操作的内存序约束编译器:
#include <atomic> #include <chrono> #include <iostream> void foo() { std::atomic<std::chrono::system_clock::time_point> timeStart = std::chrono::system_clock::now(); // 要分析性能的代码..... std::atomic<std::chrono::system_clock::time_point> timeEnd = std::chrono::system_clock::now(); std::cout << "耗时: " << (timeEnd.load() - timeStart.load()).count() << "\n"; }
原子操作的默认内存序就是memory_order_seq_cst,会阻止编译器在原子操作前后乱排指令,保证timeStart的读取一定在被测代码前,timeEnd一定在被测代码后。
3. 编译器特定的优化屏障(单平台可用)
如果不需要跨平台,也可以用编译器内置的指令,比如:
- GCC/Clang:
__asm__ __volatile__("" ::: "memory"); - MSVC:
_ReadWriteBarrier();
拿GCC举例子:
void foo() { const auto timeStart = std::chrono::system_clock::now(); __asm__ __volatile__("" ::: "memory"); // 告诉编译器别乱排前后的内存操作 // 要分析性能的代码..... __asm__ __volatile__("" ::: "memory"); const auto timeEnd = std::chrono::system_clock::now(); std::cout << "耗时: " << (timeEnd - timeStart).count() << "\n"; }
这个空汇编指令会让编译器认为内存内容可能被修改,因此不会把前后的代码跨过这个屏障重排,同样是轻量级的。
别乱用全内存屏障
全内存屏障(比如std::atomic_thread_fence(std::memory_order_seq_cst))是解决多线程之间的内存可见性问题的,单线程里用纯属杀鸡用牛刀,会平白增加性能开销。我们只需要约束编译器的指令重排,不需要管硬件的内存排序,所以用上面说的轻量方法就够了。
最后总结
- 极端优化下编译器确实可能重排计时代码,得防;
- 优先用跨平台的
std::atomic_signal_fence,性能影响最小; - 跨平台场景别用编译器特定指令,单平台可以按需选择;
- 全内存屏障完全没必要,除非涉及多线程内存可见性。
内容的提问来源于stack exchange,提问作者DDG

