为什么__sync_add_and_fetch适用于32位系统上的64位变量?

kay*_*kay 14 c x86 gcc synchronization

请考虑以下压缩代码:

/* Compile: gcc -pthread -m32 -ansi x.c */
#include <stdio.h>
#include <inttypes.h>
#include <pthread.h>

static volatile uint64_t v = 0;

void *func (void *x) {
    __sync_add_and_fetch (&v, 1);
    return x;
}

int main (void) {
    pthread_t t;
    pthread_create (&t, NULL, func, NULL);
    pthread_join (t, NULL);
    printf ("v = %"PRIu64"\n", v);
    return 0;
}
Run Code Online (Sandbox Code Playgroud)

我有一个uint64_t我想以原子方式递增的变量,因为该变量是多线程程序中的计数器.为了实现原子性,我使用了GCC的原子内置.

如果我编译amd64系统(-m64),生成的汇编代码很容易理解.通过使用a lock addq,处理器保证增量是原子的.

 400660:       f0 48 83 05 d7 09 20    lock addq $0x1,0x2009d7(%rip)
Run Code Online (Sandbox Code Playgroud)

但是相同的C代码在ia32系统(-m32)上产生了非常复杂的ASM代码:

804855a:       a1 28 a0 04 08          mov    0x804a028,%eax
804855f:       8b 15 2c a0 04 08       mov    0x804a02c,%edx
8048565:       89 c1                   mov    %eax,%ecx
8048567:       89 d3                   mov    %edx,%ebx
8048569:       83 c1 01                add    $0x1,%ecx
804856c:       83 d3 00                adc    $0x0,%ebx
804856f:       89 ce                   mov    %ecx,%esi
8048571:       89 d9                   mov    %ebx,%ecx
8048573:       89 f3                   mov    %esi,%ebx
8048575:       f0 0f c7 0d 28 a0 04    lock cmpxchg8b 0x804a028
804857c:       08 
804857d:       75 e6                   jne    8048565 <func+0x15>
Run Code Online (Sandbox Code Playgroud)

这是我不明白的:

  • lock cmpxchg8b 保证,如果预期值仍驻留在目标地址改变的变量只写.比较和交换保证以原子方式发生.
  • 什么保证0x804855a和0x804855f中的变量读取是原子的?

可能是否有"脏读"并不重要,但有人可以请一个简短的证据表明没有问题吗?

进一步:为什么生成的代码跳回0x8048565而不是0x804855a?我很肯定,如果其他作家也只增加变量,这只是正确的.这是__sync_add_and_fetch函数的牵连要求吗?

Nec*_*lis 17

由于它正确对齐(并且它适合一个缓存线),并且由于英特尔以这种方式制定了规范,因此保证读取是原子的,请参阅英特尔架构手册第1,4,4.1节:

跨越4字节边界的字或双字操作数或跨越8字节边界的四字操作数被认为是未对齐的,并且需要两个单独的存储器总线周期来进行访问.

第3A卷8.1.1:

奔腾处理器(以及更新的处理器)保证以下额外的内存操作将始终以原子方式执行:

•读取或写入在64位边界上对齐的四字

•16位访问非缓存存储器位置,适合32位数据总线

P6系列处理器(以及之后的新处理器)保证以下额外的内存操作将始终以原子方式执行:

•未对齐的16位,32位和64位访问缓存内存,适合缓存行

因此,通过对齐,它可以在1个周期内读取,并且它适合于一个高速缓存行,从而使读取原子化.

代码跳回,mov因为指针已经加载,不需要再次加载它们,如果失败cmpxchg8b则设置lock为目标中的值:

lock英特尔ISA手册Vol.2A:

比较EDX:EAX和m64.如果相等,则设置ZF并将ECX:EBX加载到m64.否则,清除ZF并将m64加载到EDX:EAX中.

因此,代码只需要递增新返回的值并再次尝试.如果我们在C代码中使用它变得更容易:

value = dest;                    // non-atomic but usually won't tear
while(!CAS8B(&dest,value,value + 1))
{
    value = dest;                // atomic; part of lock cmpxchg8b
}
Run Code Online (Sandbox Code Playgroud)