編譯器對外部變量的處理

一直以來對鎖比較感興趣。因爲在多核編程中,鎖是一個可恨有可愛的東西。說它可恨,是因爲鎖的使用,降低了併發性,也就降低了性能。可愛之處呢,因爲鎖的使用有時是無法避免的。那麼如何實現一個高性能的鎖又是一個很有意思的問題。以後有機會,再跟大家交流一下鎖的實現部分。


今天是我在看spinlock的時候,突然想到的問題。這個問題不侷限於spinlock,下面的示例我使用mutex來代替。


#include <stdlib.h>

#include <stdio.h>

#include <pthread.h>


extern int counter;

extern pthread_mutex_t counter_mutex;


void add_counter(void)

{

    pthread_mutex_lock(&counter_mutex);

    ++counter;

    pthread_mutex_unlock(&counter_mutex);

}

counter是由counter_mutex保護的。在更新counter的時候,必須要先持有counter_mutex,這樣才能保證正確更新counter。另外鎖的實現中,一般需要有內存barrier的指令,來禁止CPU的亂序執行。如果沒有barrier的話,在CPU的指令執行過程中,counter的更新很可能發生在unlock之後。這些問題也不是今天的重點。


內存barrier指令只能保證CPU在barrier前的內存指令必須完成。但是如果在編譯器將counter放到了寄存器中怎麼辦?比如在持有counter_mutex之前,對counter有讀取的操作。那麼編譯器很可能會將counter在lock之前,就存到寄存器中。這樣,在持有lock之後,counter因爲之前已經讀取到寄存器中了,這個++counter是否會直接對寄存器操作呢?如下面的代碼:


#include <stdlib.h>

#include <stdio.h>

#include <pthread.h>


extern int counter;

extern pthread_mutex_t counter_mutex;


#define ASM_SEPERATOR __asm__ __volatile__ ("nop")


void add_counter(void)

{

    /* 下面的代碼對counter進行了讀取操作,那麼counter會被放入到寄存器中 */

    int t = counter;

    ASM_SEPERATOR;

    printf("counter is %d\n", counter);

    ASM_SEPERATOR;


     /* 在前面的代碼中,counter已經被放入到了寄存器中,那麼下面的更新是否會直接更新該寄存器呢? */

    pthread_mutex_lock(&counter_mutex);

    ASM_SEPERATOR;

    ++counter;

    ASM_SEPERATOR;

    pthread_mutex_unlock(&counter_mutex);

}


當想到這個問題的時候,心裏升起一陣寒意。因爲這樣的代碼肯定會存在於我們的工程中。在持有鎖之前,對保護的資源進行讀取的動作,這是一個很平常的行爲。如果前面的讀取動作導致該資源被放到寄存器中,豈不是導致鎖失效了?難道在這種情況下,即使是讀取動作也要加鎖保護嗎?如果假設爲真的話,那麼有bug的代碼就太多了,那麼早就報出很多問題了。所以這種使用方法應該是沒有問題的。


還是讓我們看一下反彙編吧:


00000000 :

extern pthread_mutex_t counter_mutex;

#define ASM_SEPERATOR __asm__ __volatile__ ("nop")

void add_counter(void)

{

0: 55 push %ebp

1: 89 e5 mov %esp,%ebp

3: 83 ec 28 sub $0x28,%esp

int t = counter;

6: a1 00 00 00 00 mov 0x0,%eax

b: 89 45 f4 mov %eax,-0xc(%ebp)

ASM_SEPERATOR;

e: 90 nop

printf("counter is %d\n", counter);

f: 8b 15 00 00 00 00 mov 0x0,%edx

15: b8 00 00 00 00 mov $0x0,%eax

1a: 89 54 24 04 mov %edx,0x4(%esp)

1e: 89 04 24 mov %eax,(%esp)

21: e8 fc ff ff ff call 22

ASM_SEPERATOR;

26: 90 nop

27: c7 04 24 00 00 00 00 movl $0x0,(%esp)

2e: e8 fc ff ff ff call 2f

pthread_mutex_lock(&counter_mutex);

33: 90 nop

ASM_SEPERATOR;

34: a1 00 00 00 00 mov 0x0,%eax

39: 83 c0 01 add $0x1,%eax

3c: a3 00 00 00 00 mov %eax,0x0

++counter;

41: 90 nop

ASM_SEPERATOR;

42: c7 04 24 00 00 00 00 movl $0x0,(%esp)

49: e8 fc ff ff ff call 4a

pthread_mutex_unlock(&counter_mutex);

4e: c9 leave

4f: c3 ret

紅色部分的代碼是將counter賦給t,這時counter已經被存入到eax中。而藍色的代碼是++counter。這裏顯示在counter進行自加的時候,是重新讀取counter到寄存器中,然後再做自加,並沒有直接利用前面的寄存器eax。


上面的彙編是沒有使用優化選項的輸出,下面是使用-O2優化的彙編結果:


Disassembly of section .text:

00000000 :

0: 55 push %ebp

1: 89 e5 mov %esp,%ebp

3: 83 ec 18 sub $0x18,%esp

6: 90 nop

7: a1 00 00 00 00 mov 0x0,%eax

c: c7 04 24 00 00 00 00 movl $0x0,(%esp)

13: 89 44 24 04 mov %eax,0x4(%esp)

17: e8 fc ff ff ff call 18

1c: 90 nop

1d: c7 04 24 00 00 00 00 movl $0x0,(%esp)

24: e8 fc ff ff ff call 25

29: 90 nop

2a: 83 05 00 00 00 00 01 addl $0x1,0x0

31: 90 nop

32: c7 04 24 00 00 00 00 movl $0x0,(%esp)

39: e8 fc ff ff ff call 3a

3e: c9 leave

3f: c3 ret

在t=counter時,依然是將counter放入到eax中,然後在將eax的值賦給t。而++counter的時候,乾脆不用寄存器了,直接對內存進行加1的操作(x86支持對內存的加法操作)。


從彙編的結果上看,我之前的想到的問題有些杞人憂天了。即使counter在lock之前被存入某個寄存器,在自加的時候,仍然會重新讀取,而不是直接使用那個寄存器。那麼爲什麼編譯器會產生這樣的結果呢?因爲使用了lock?比如lock的API中會有某個指令導致編譯器生成這樣的代碼?我認爲不可能。因爲這樣對編譯器提出了非常過分的要求。因爲編譯的時候,編譯器根本不會去檢查調用的函數。在本例中,這個函數是pthread庫函數,但是很多時候,這個函數甚至可以不存在。所以這個猜想肯定不對的。那麼只有一個合理的解釋了,因爲counter是一個外部變量(非本函數內部定義)。編譯器會假設該變量可能隨時都會被外部更改,所以在任何時候,都需要重新讀取到寄存器再使用。


這次我們乾脆不是用全局變量,而是使用傳入的參數:

#include <stdlib.h>

#include <stdio.h>

#include <pthread.h>


#define ASM_SEPERATOR __asm__ __volatile__ ("nop")


void add_counter(int *counter)

{

    int t = *counter;

    ASM_SEPERATOR;

    printf("counter is %d %d\n", t, *counter);

    ASM_SEPERATOR;


    ASM_SEPERATOR;

    ++*counter;

    ASM_SEPERATOR;

    printf("counter is %d\n", *counter);


}

反彙編輸出:


00000000 :

0: 55 push %ebp

1: 89 e5 mov %esp,%ebp

3: 53 push %ebx

4: 83 ec 14 sub $0x14,%esp

7: 8b 5d 08 mov 0x8(%ebp),%ebx

a: 8b 03 mov (%ebx),%eax

c: 90 nop

d: 89 44 24 08 mov %eax,0x8(%esp)

11: 89 44 24 04 mov %eax,0x4(%esp)

15: c7 04 24 00 00 00 00 movl $0x0,(%esp)

1c: e8 fc ff ff ff call 1d

21: 90 nop

22: 90 nop

23: 8b 03 mov (%ebx),%eax

25: 83 c0 01 add $0x1,%eax

28: 89 03 mov %eax,(%ebx)

2a: 90 nop

2b: 89 44 24 04 mov %eax,0x4(%esp)

2f: c7 04 24 12 00 00 00 movl $0x12,(%esp)

36: e8 fc ff ff ff call 37

3b: 83 c4 14 add $0x14,%esp

3e: 5b pop %ebx

3f: 5d pop %ebp

40: c3 ret

藍色部分仍然是自加的代碼++*counter,和全局變量的counter一樣,都是需要將外部變量的值讀入到寄存器中,然後進行運算,再存入到寄存器中。


至此,我們得出結論,編譯器在處理外部變量的時候,每次都需要重新讀取到寄存器中,然後再使用。


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章