如何在x86汇编中原子性移动64位值?32位程序中原子赋值双精度浮点数可行吗?
解决方案:32位应用中原子性赋值64位double值
好问题!刚好之前在做32位程序的跨线程原子操作时研究过这个场景,给你梳理下可行的方案:
首先明确前提:你提到的Intel手册内容完全正确——奔腾及后续的x86处理器,对对齐在64位(8字节)边界上的四字(64位)内存操作,无论是读还是写,都保证原子性。32位模式下虽然默认寄存器是32位,但可以通过汇编指令实现单次64位写操作,从而达成原子赋值的目标。
核心要点:内存对齐是关键
不管用哪种方法,首先必须确保你的double变量严格对齐到8字节。如果变量不对齐,即使是64位操作也会被处理器拆分成两次32位内存访问,直接失去原子性。
在C代码里可以通过编译器属性强制对齐:
- GCC/Clang:
__attribute__((aligned(8))) double my_double; - MSVC:
__declspec(align(8)) double my_double;
方案一:直接用movq指令实现高效原子赋值
这是最简单高效的方式,前提是处理器支持MMX指令集(奔腾及以后都支持),且变量已对齐到8字节。
嵌入汇编示例(GCC/Clang)
#include <stdint.h> // 强制8字节对齐的double变量 __attribute__((aligned(8))) double my_double; void atomic_set_double(double val) { // 把double转成uint64_t,避免浮点指令干扰 uint64_t val64 = *(uint64_t*)&val; __asm__ __volatile__ ( "movq %0, %%mm0\n" // 把64位值加载到MMX寄存器mm0 "movq %%mm0, %1\n" // 从mm0原子写入对齐的内存 : : "r"(val64), "m"(my_double) : "mm0", "memory" // 告诉编译器这些寄存器/内存被修改了 ); }
或者用32位寄存器组合的方式(不需要MMX,同样要求对齐):
void atomic_set_double(double val) { uint64_t val64 = *(uint64_t*)&val; uint32_t low = (uint32_t)val64; uint32_t high = (uint32_t)(val64 >> 32); __asm__ __volatile__ ( "movl %0, %%eax\n" "movl %1, %%edx\n" "movq %%edx:%%eax, %2\n" // 把edx:eax组成的64位值原子写入内存 : : "r"(low), "r"(high), "m"(my_double) : "eax", "edx", "memory" ); }
嵌入汇编示例(MSVC)
; 假设my_double是已对齐的全局变量 mov eax, 09ABCDEF0h ; double值的低32位 mov edx, 012345678h ; double值的高32位 mov qword ptr [my_double], edx:eax ; 原子写入64位值
方案二:用lock cmpxchg8b实现通用原子赋值
如果无法保证变量对齐,或者想要兼容性更强的方案(虽然本质上还是奔腾及以后支持),可以用带lock前缀的cmpxchg8b指令。这个指令是原子比较交换操作,我们可以通过循环来实现原子赋值:
GCC示例
#include <stdint.h> double my_double; // 不需要强制对齐 void atomic_set_double(double val) { uint64_t val64 = *(uint64_t*)&val; uint32_t low = (uint32_t)val64; uint32_t high = (uint32_t)(val64 >> 32); __asm__ __volatile__ ( "1:\n" ; 循环标签 ; 先读取当前内存里的64位值到ebx:eax "movq %2, %%mm0\n" "movd %%mm0, %%ebx\n" "psrlq $32, %%mm0\n" "movd %%mm0, %%eax\n" ; 设置要写入的新值到ecx:edx "movl %0, %%ecx\n" "movl %1, %%edx\n" ; 原子比较交换:如果内存值等于ebx:eax,就写入ecx:edx;否则更新ebx:eax为内存值 "lock cmpxchg8b %3\n" "jne 1b\n" ; 如果交换失败,循环重试 : : "r"(low), "r"(high), "m"(my_double), "m"(my_double) : "eax", "ebx", "ecx", "edx", "mm0", "memory" ); }
这个方法的缺点是比movq慢一些(因为可能有循环重试),但好处是不需要严格对齐,且能应对多线程并发修改的场景(即使其他线程同时修改变量,循环也会重试直到成功)。
最后补充
- 所有方案都要求处理器是奔腾及以后的型号,这在现代环境里基本不是问题;
- 如果是跨平台场景,要注意不同编译器的汇编语法差异;
- 如果你只是需要原子赋值(而不是原子交换/加减),对齐+
movq是最优选择。
内容的提问来源于stack exchange,提问作者Sargis
相关产品推荐
相关产品推荐

