1. 程式人生 > 其它 >程式碼重排序

程式碼重排序

原始碼和Runtime時執行的程式碼很可能不一樣,這是因為編譯器、處理器常常會為了追求效能對改變執行順序。然而改變順序執行很危險,很有可能使得執行結果和預想的不一樣,特別是當重排序共享變數時。從原始碼到Runtime需要經過三步的重排序:

1、編譯器重排序

為了提高效能,在不改變單執行緒的執行結果下,可以改變語句執行順序。比如儘可能的減少暫存器的讀寫次數,充分利用區域性性。像下面這段程式碼這樣,交替的讀x、y,會導致暫存器頻繁的交替儲存x和y,最糟的情況下暫存器要儲存3次x和3次y。如果能讓x的一系列操作一塊做完,y的一塊做完,理想情況下暫存器只需要儲存1次x和1次y。

//優化前
int x = 1
; int y = 2; int a1 = x * 1; int b1 = y * 1; int a2 = x * 2; int b2 = y * 2; int a3 = x * 3; int b3 = y * 3; //優化後 int x = 1; int y = 2; int a1 = x * 1; int a2 = x * 2; int a3 = x * 3; int b1 = y * 1; int b2 = y * 2; int b3 = y * 3;

2、指令重排序

指令重排序是處理器層面做的優化。處理器在執行時往往會因為一些限制而等待,如訪存的地址在cache中未命中,這時就需要到記憶體甚至外存去取,然而記憶體和外區的讀取速度比CPU執行速度慢得多。
早期處理器是順序執行(in-order execution)的,在記憶體、外存讀取資料這段時間,處理器就一直處於等待狀態。現在處理器一般都是亂序執行(out-of-order execution),處理器會在等待資料的時候去執行其他已準備好的操作,不會讓處理器一直等待。
滿足亂序執行的條件:

  • 該快取的運算元快取好
  • 有空閒的執行單元

對於下面這段彙編程式碼,操作1如果發生cache miss,則需要等待讀取記憶體外存。看看有沒有能優先執行的指令,操作2依賴於操作1,不能被優先執行,操作3不依賴1和2,所以能優先執行操作3。
所以實際執行順序是3>1>2

LDR R1, [R0];//操作1
ADD R2, R1, R1;//操作2
ADD R3, R4, R4;//操作3

3、記憶體系統重排序

由於處理器有讀、寫快取區。寫快取區沒有及時重新整理到記憶體,造成其他處理器讀到的值不是最新的,使得處理器執行的讀寫操作與記憶體上反應出的順序不一致。
如下面這個例子,可能造成處理器A讀到的b=0,處理器B讀到的a=0。A1寫a=1先寫到處理器A的寫快取區中,此時記憶體中a=0。如果這時處理器B從記憶體中讀a,讀到的將是0。

初始化:
a = 0;
b = 0;

處理器A執行
a = 1; //A1
read(b); //A2

處理器B執行
b = 2; //B1
read(a); //B2

4、阻止重排序

不論哪種重排序都可能造成共享變數中執行緒間不可見,這會改變程式執行結果。所以需要禁止對那些要求可見的共享變數重排序。

  • 阻止編譯重排序:禁止編譯器在某些時候重排序。
  • 阻止指令重排序和記憶體系統重排序:使用記憶體屏障或Lock字首指令。