GCC下的64位原子写操作

12

我在多线程编程方面陷入了一团混乱,希望有人能够来帮我理解一下。

在阅读了很多资料后,我明白了在64位系统上应该能够原子地设置64位int类型的值1

但是,对于这些资料中的很多内容我感到困惑,所以我尝试编写一个简单的程序来验证。我编写了一个只有一个线程的简单程序,该程序将变量设置为两个值中的一个:

bool switcher = false;

while(true)
{
    if (switcher)
        foo = a;
    else
        foo = b;
    switcher = !switcher;
}

还有一个线程会检查 foo 的值:

while (true)
{
    __uint64_t blah = foo;
    if ((blah != a) && (blah != b))
    {
        cout << "Not atomic! " << blah << endl;
    }
}

我设置了 a = 1844674407370955161;b = 1144644202170355111;。运行程序后,没有任何输出警告我 blah 不是 ab

看起来很好,似乎这是一个原子写操作...但是,接下来我改变了第一个线程直接设置 ab,代码如下:

bool switcher = false;

while(true)
{
    if (switcher)
        foo = 1844674407370955161;
    else
        foo = 1144644202170355111;
    switcher = !switcher;
}

我重新运行,突然出现:

Not atomic! 1144644203261303193
Not atomic! 1844674406280007079
Not atomic! 1144644203261303193
Not atomic! 1844674406280007079
什么改变了?无论如何,我都给foo赋了一个很大的数字 - 编译器是否会以不同的方式处理常数,或者我误解了一切?
谢谢!
1:Intel CPU文档,第8.1节,保证原子操作 2:GCC开发人员讨论GCC在文档中没有保证,但内核和其他程序依赖它

编译时有没有收到任何警告? - Nim
我不认为这是罪魁祸首,但字面值默认为int类型,因此您需要将1844674407370955161ULL和1144644202170355111ULL作为字面值。 - etarion
Nim编译时没有警告,并且设置了-Wall。 - Frederik
etarion,如果这些数字多1位数字,则会收到警告,并需要使用ULL来消除警告。无论哪种方式,都会发生相同的情况。 - Frederik
你无法可靠地将CPU保证翻译成C/C++保证,因为你不知道编译器会做什么。 - David Schwartz
3个回答

12

反汇编循环后,我使用 gcc 得到以下代码:

.globl _switcher
_switcher:
LFB2:
    pushq   %rbp
LCFI0:
    movq    %rsp, %rbp
LCFI1:
    movl    $0, -4(%rbp)
L2:
    cmpl    $0, -4(%rbp)
    je  L3
    movq    _foo@GOTPCREL(%rip), %rax
    movl    $-1717986919, (%rax)
    movl    $429496729, 4(%rax)
    jmp L5
L3:
    movq    _foo@GOTPCREL(%rip), %rax
    movl    $1486032295, (%rax)
    movl    $266508246, 4(%rax)
L5:
    cmpl    $0, -4(%rbp)
    sete    %al
    movzbl  %al, %eax
    movl    %eax, -4(%rbp)
    jmp L2
LFE2:

看起来 gcc 确实使用32位的 movl 指令与32位的立即数值。有一条指令 movq 可以将64位寄存器移动到内存(或内存移动到64位寄存器),但似乎不能将立即数值设置为内存地址,因此编译器被迫要么使用临时寄存器然后将值移动到内存中,要么使用 movl。你可以尝试使用临时变量强制它使用寄存器,但这可能不会奏效。

参考文献:


有趣!感谢您抽出时间来分解它! - Frederik
你使用的编译器版本、平台和编译器选项是什么?这会有很大的区别。如果对象是8字节对齐并且系统正在运行64位代码,那么64位写入将只是原子性的,如果在32位模式下运行(操作系统为32位或操作系统为64位但二进制文件为32位),则写入将不是原子性的。 - David Rodríguez - dribeas
1
GCC 4.2,MacOS X,CPU核心i7,操作系统为64位,代码编译为x86_64架构。写入64位值是原子性的,但无法在操作码中表示64位立即值,正如@AProgrammer所指出的那样。因此,编译器必须在将其移动到内存之前将立即值复制到寄存器中,或者它必须非原子地存储该值,复制立即值的两个32位半部分。 - Sylvain Defresne

12

http://www.x86-64.org/documentation/assembly.html

指令内的立即值保持32位。

编译器无法原子地执行64位常量的赋值,除非先将其填充到寄存器中,然后将该寄存器移动到变量中。 这可能比直接赋值给变量更加费时,并且由于语言不需要原子性,因此不选择原子解决方案。


另一个很棒的回复!谢谢! - Frederik
如果有同时包含8字节值和8字节地址的mov immediate指令,那么生成的指令大小将非常糟糕。可以理解为什么他们不想为此构建解码器! - Bo Persson

3

英特尔CPU文档表明,最近的硬件上,对齐8字节的读/写操作始终是原子性的(即使在32位操作系统上)。

但您没有告诉我们的是,您是否在32位系统上使用64位硬件?如果是这样的话,编译器很可能会将8字节写操作拆分为两个4字节的写操作。

请查看目标代码中相关的部分。


你好,drhirsch。该系统是64位的Linux。uname输出为:Linux acorn 2.6.35-25-server #44 SMP Fri Feb 11 15:50:10 GMT 2011 x86_64 GNU/Linux。 - Frederik

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接