考虑以下简明代码:
/* 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
确保保证只有在预期值仍位于目标地址中时才会写入更改的变量。比较和交换保证以原子方式发生。如果有“脏读”可能无关紧要,但是有人可以请一个简短的证明来表明没有问题吗?
此外:为什么生成的代码会跳回0x8048565而不是0x804855a?我很肯定,如果其他作家也只增加变量,这只是正确的。这是__sync_add_and_fetch
函数的牵连要求吗?
答案 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)))
。