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

时间:2012-02-27 15:46:38

标签: c gcc synchronization ia-32

考虑以下简明代码:

/* 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;
}

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

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

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

但是相同的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>

这是我不明白的地方:

  • lock cmpxchg8b 确保保证只有在预期值仍位于目标地址中时才会写入更改的变量。比较和交换保证以原子方式发生。
  • 但是什么能保证读取0x804855a和0x804855f中的变量是原子的?

如果有“脏读”可能无关紧要,但是有人可以请一个简短的证明来表明没有问题吗?

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

2 个答案:

答案 0 :(得分:17)

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

  

跨越4字节边界或a的字或双字操作数   考虑跨越8字节边界的四字操作数   未对齐,需要两个独立的存储器总线周期才能访问。

第3A卷8.1.1:

  

奔腾处理器(以及更新的处理器)确保了   将始终执行以下附加内存操作   原子地:

     

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

     

•16位访问适合的未缓存内存位置   在32位数据总线中

     

P6系列处理器(以及更新版本   处理器自)保证以下额外的内存   操作将始终以原子方式进行:

     

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

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

代码跳回0x8048565因为已经加载了指针,因此无需再次加载它们,因为CMPXCHG8B会将EAX:EDX设置为目标中的值它失败了:

CMPXCHG8B英特尔ISA手册Vol。 2A:

  

将EDX:EAX与m64进行比较。如果相等,则设置ZF并将ECX:EBX加载到m64。   否则,清除ZF并将m64加载到EDX:EAX。

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

value = dest;
While(!CAS8B(&dest,value,value + 1))
{
    value = dest;
}

答案 1 :(得分:3)

0x804855a和0x804855f中的变量读取不需要是原子的。使用比较和交换指令在伪代码中递增:

oldValue = *dest;
do {
    newValue = oldValue+1;
} while (!compare_and_swap(dest, &oldValue, newValue));

由于比较和交换在交换之前检查*dest == oldValue,它将作为安全措施 - 因此如果oldValue中的值不正确,将再次尝试循环,所以有如果非原子读数导致值不正确,则没问题。

你的第二个问题是为什么行oldValue = *dest不在循环中。这是因为compare_and_swap函数始终会将oldValue的值替换为*dest的实际值。所以它基本上会为你执行oldValue = *dest行,并且没有必要再做一次。在cmpxchg8b指令的情况下,当比较失败时,它会将内存操作数的内容放在edx:eax中。

compare_and_swap的伪代码是:

bool compare_and_swap (int *dest, int *oldVal, int newVal)
{
  do atomically {
    if ( *oldVal == *dest ) {
        *dest = newVal;
        return true;
    } else {
        *oldVal = *dest;
        return false;
    }
  }
}

顺便说一句,在您的代码中,您需要确保v与64位对齐 - 否则它可以在两个缓存行之间拆分,并且cmpxchg8b指令不会以原子方式执行。您可以使用GCC的__attribute__((aligned(8)))