2012-02-27 244 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)))這個。

相關問題