2012-02-27 247 views
11

考虑以下浓缩代码:为什么__sync_add_and_fetch适用于32位系统上的64位变量?

/* 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系统上一个非常复杂的ASM代码(-m32):

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函数的一个暗示要求吗?

回答

16

读取被保证是原子由于它被正确地对齐(并且它适合于一个高速缓存行),并且因为由英特尔规范这种方式,请参阅英特尔架构手册第1卷,4.4.1:

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

第3A卷8.1。1:

奔腾处理器(和自更新的处理器)保证了 以下附加的存储器操作将总是被原子地进行 :

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

•16位访问到一个32位数据总线内适合 未缓存的存储器位置

P6系列处理器(和由于较新的 处理器)保证下面的附加存储器 操作将总是被原子地进行:

•未对齐16,32, 和64位访问,以适应的高速缓存行内的高速缓存的存储器

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

代码跳回0x8048565因为指针已经被加载,就没有必要再加载它们,如CMPXCHG8B将在目标设定EAX:EDX的值,如果它失败:

CMPXCHG8B描述为英特尔ISA手册卷。 2A:

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

因此,代码只需要增加新返回的值,然后重试。 如果我们这样的在C代码变得更加容易:

value = dest; 
While(!CAS8B(&dest,value,value + 1)) 
{ 
    value = dest; 
} 
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)))这个。

相关问题