为什么__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)))
。