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

请考虑以下压缩代码:

/* Compile: gcc -pthread -m32 -ansi xc */ #include  #include  #include  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变量,我想以primefaces方式递增,因为该变量是multithreading程序中的计数器。 为了达到primefaces性,我使用了GCC的primefaces内核 。

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

  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  

这是我不明白的:

  • lock cmpxchg8b确保仅在期望值仍驻留在目标地址中时才写入已更改的变量。 比较和交换保证以primefaces方式发生。
  • 但是什么保证0x804855a和0x804855f中的变量读取是primefaces的?

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

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

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

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

第3A卷8.1.1:

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

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

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

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

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

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

代码跳回到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; } 

在0x804855a和0x804855f中读取变量不需要是primefaces的。 使用比较和交换指令在伪代码中增加如下所示:

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

由于比较和交换在交换之前检查*dest == oldValue ,它将作为安全措施 – 因此如果oldValue的值不正确,将再次尝试循环,因此如果非primefaces的话没有问题读取导致错误的值。

你的第二个问题是为什么行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指令不会以primefaces方式执行。 您可以使用GCC的__attribute__((aligned(8)))