为何MSVC在x64平台的原子加载操作中生成NOP指令?
MSVC下atomic acquire加载后生成NOP的成因与解决方法
编译以下C++代码时,MSVC会在每次std::memory_order_acquire语义的原子加载后生成NOP填充:
#include <atomic> int load(std::atomic<int> *p) { return p->load(std::memory_order_acquire) + p->load(std::memory_order_acquire); }
对应的汇编代码如下:
int load(std::atomic<int> *) PROC mov edx, DWORD PTR [rcx] npad 1 mov eax, DWORD PTR [rcx] npad 1 add eax, edx ret 0
一、现象成因
这是MSVC针对x86/x64架构的硬件特性优化策略导致的:
- x86/x64架构的普通加载操作天然具备
acquire语义(加载操作不会被重排到后续指令之后),因此std::memory_order_acquire在这里不需要额外的内存屏障指令。 - MSVC生成的
npad(NOP填充)是为了指令对齐:CPU取指时若指令跨缓存行,会带来额外的拆分开销,通过NOP填充将后续指令对齐到缓存行边界,能提升高频执行代码的性能。 - MSVC的代码生成器对带有
acquire语义的加载操作会强制插入对齐填充,这是它的一种保守性能优化策略,确保原子操作相关指令的执行效率。
二、不放松memory_order前提下避免NOP的方法
可以通过以下几种方式在保留acquire语义的同时避免生成多余NOP:
调整编译器优化级别
启用/O2(最高优化级别)后,MSVC会根据代码实际执行场景动态调整对齐策略,自动移除不必要的NOP。测试显示,/O2下该代码的汇编输出会去掉npad指令。
若需保留低优化级别,可尝试使用#pragma optimize("g", off)临时关闭局部代码的优化,但该操作可能影响其他代码性能,需谨慎使用。合并原子加载操作(业务场景允许时)
如果两次加载的预期结果一致,可将两次acquire加载合并为一次,复用加载结果:int load(std::atomic<int> *p) { int val = p->load(std::memory_order_acquire); return val + val; }这种方式既保留了
acquire语义,又仅触发一次原子加载,自然不会生成多余NOP。用内存栅栏替代加载的acquire语义
将acquire语义通过std::atomic_thread_fence实现,加载操作使用relaxed语义,MSVC不会对relaxed加载插入NOP:int load(std::atomic<int> *p) { int val1 = p->load(std::memory_order_relaxed); std::atomic_thread_fence(std::memory_order_acquire); int val2 = p->load(std::memory_order_relaxed); std::atomic_thread_fence(std::memory_order_acquire); return val1 + val2; }注意内存栅栏本身会带来一定性能开销,需根据实际场景权衡使用。
内容的提问来源于stack exchange,提问作者user541686
相关产品推荐
相关产品推荐

