為什麼要指令重排序?

Mageek Chiu發表於2018-05-28

我們知道java在執行的時候有兩個地方可能用到重排序,一個是編譯器編譯的的時候,一個是處理器執行的時候。 那麼我們就應該問問為啥要用指令重排序呢?

生活類比

我們從生活中舉個例子,假設你有一箱紅紙,現在要你剪成小紅花貼在窗上。你有兩種極端的選擇:拿出來一個,把這個剪好,再貼上去......一個一個依次進行;另一種方式是先全部拿出來,然後全部剪好,最後全部貼上去。

那種效率更高?很明顯是後者,因為前者你就需要不停地在箱子,剪刀和膠水之間切換,這個切換過程不僅浪費時間,還耗費精力。但是後者一直做一個工作也很無聊,還會導致半天了窗上一朵花都沒有,會給你帶來失落感,所以比較合適的做法就是拿出來一疊,把這一疊剪好,貼上去。這樣既不無聊,也減少了切換次數,提高了工作效率。

再想想,如果有三個人,一個負責拿,一個負責剪,一個負責貼,就更快了。

分析

編譯期重排序有啥好處?CPU計算的時候要訪問值,如果常常利用到暫存器中已有的值就不用去記憶體讀取了,比如說

int a = 1;
int b = 1;
a = a + 1;
b = b +1 ;
複製程式碼

就可能沒有

int a = 1;
a = a + 1;
int b = 1;
b = b +1 ;
複製程式碼

效能好,因為後者可以 a或b可能在暫存器中了。

處理器為啥要重排序?因為一個彙編指令也會涉及到很多步驟,每個步驟可能會用到不同的暫存器,CPU使用了流水線技術,也就是說,CPU有多個功能單元(如獲取、解碼、運算和結果),一條指令也分為多個單元,那麼第一條指令執行還沒完畢,就可以執行第二條指令,前提是這兩條指令功能單元相同或類似,所以一般可以通過指令重排使得具有相似功能單元的指令接連執行來減少流水線中斷的情況。

我們寫一段程式碼來試試:

package *****;

/**
 * reorder
 * @author Mageek Chiu
 * @date 2018/5/25 0025:12:49
 */
public class ReOrder {

    public int value ;

    private ReOrder(int value) {
        this.value = value;
    }

    public static void main(String... args){
        ReOrder reOrder = new ReOrder(111);
        ReOrder reOrder1 = new ReOrder(222);
        ReOrder reOrder2 = new ReOrder(333);
        System.out.println(add1(reOrder,reOrder1,reOrder2));
    }

    static int add1(ReOrder reOrder,ReOrder reOrder1,ReOrder reOrder2){
        int result = 0;

        result += reOrder.value;
        result += reOrder1.value;
        result += reOrder2.value;//***

        result += reOrder.value;
        result += reOrder1.value;
        result += reOrder2.value;

        result += reOrder.value;
        result += reOrder1.value;
        result += reOrder2.value;

        return result;

    }

}
複製程式碼

執行結果中:

 # {method} {0x000000001c402c80} 'add1' '(*****/ReOrder;*****/ReOrder;*****/ReOrder;)I' in '*****/ReOrder'
  # parm0:    rdx:rdx   = '*****/ReOrder'
  # parm1:    r8:r8     = '*****/ReOrder'
  # parm2:    r9:r9     = '*****/ReOrder'
  #           [sp+0x20]  (sp of caller)
  0x00000000032a86c0: mov     dword ptr [rsp+0ffffffffffffa000h],eax
  0x00000000032a86c7: push    rbp
  0x00000000032a86c8: sub     rsp,10h           ;*synchronization entry
                                                ; - *****.ReOrder::add1@-1 (line 24)

  0x00000000032a86cc: mov     r11d,dword ptr [rdx+0ch]
                                                ;*getfield value
                                                ; - *****.ReOrder::add1@4 (line 26)
                                                ; implicit exception: dispatches to 0x00000000032a86ff
  0x00000000032a86d0: mov     r10d,dword ptr [r8+0ch]  ;*getfield value
                                                ; - *****.ReOrder::add1@11 (line 27)
                                                ; implicit exception: dispatches to 0x00000000032a870d
  0x00000000032a86d4: mov     r9d,dword ptr [r9+0ch]  ;*getfield value
                                                ; - *****.ReOrder::add1@18 (line 28)
                                                ; implicit exception: dispatches to 0x00000000032a8719
  0x00000000032a86d8: mov     eax,r11d
  0x00000000032a86db: add     eax,r10d
  0x00000000032a86de: add     eax,r9d
  0x00000000032a86e1: add     eax,r11d
  0x00000000032a86e4: add     eax,r10d
  0x00000000032a86e7: add     eax,r9d
  0x00000000032a86ea: add     eax,r11d
  0x00000000032a86ed: add     eax,r10d
  0x00000000032a86f0: add     eax,r9d           ;*iadd
複製程式碼

也就是先用mov把方法裡面所需要的三個value載入了,再統一用add進行加法運算。

現在我們把//***哪一行註釋掉,執行結果如下:

[Constants]
  # {method} {0x000000001c052c78} 'add1' '(*****/ReOrder;*****/ReOrder;*****/ReOrder;)I' in '*****/ReOrder'
  # parm0:    rdx:rdx   = '*****/ReOrder'
  # parm1:    r8:r8     = '*****/ReOrder'
  # parm2:    r9:r9     = '*****/ReOrder'
  #           [sp+0x20]  (sp of caller)
  0x0000000002f47d40: mov     dword ptr [rsp+0ffffffffffffa000h],eax
  0x0000000002f47d47: push    rbp
  0x0000000002f47d48: sub     rsp,10h           ;*synchronization entry
                                                ; - *****.ReOrder::add1@-1 (line 24)

  0x0000000002f47d4c: mov     r11d,dword ptr [rdx+0ch]
                                                ;*getfield value
                                                ; - *****r.ReOrder::add1@4 (line 26)
                                                ; implicit exception: dispatches to 0x0000000002f47d7c
  0x0000000002f47d50: mov     r10d,dword ptr [r8+0ch]  ;*getfield value
                                                ; - *****.ReOrder::add1@11 (line 27)
                                                ; implicit exception: dispatches to 0x0000000002f47d89
  0x0000000002f47d54: mov     r9d,dword ptr [r9+0ch]  ;*getfield value
                                                ; - *****::add1@32 (line 32)
                                                ; implicit exception: dispatches to 0x0000000002f47d95
  0x0000000002f47d58: mov     eax,r11d
  0x0000000002f47d5b: add     eax,r10d
  0x0000000002f47d5e: add     eax,r11d
  0x0000000002f47d61: add     eax,r10d
  0x0000000002f47d64: add     eax,r9d
  0x0000000002f47d67: add     eax,r11d
  0x0000000002f47d6a: add     eax,r10d
  0x0000000002f47d6d: add     eax,r9d           ;*iadd
複製程式碼

依然是先把所有value都用mov指令載入後再進行加法運算。 總結起來就是不管程式碼裡這個值使用順序多靠後,都先用mov載入後再使用add對這個值進行運算。

注意,上面的執行引數為-Xcomp -XX:+UnlockDiagnosticVMOptions -XX:CompileCommand=print,*ReOrder.add1 -XX:+PrintCompilationXcomp 含義是使用編譯模式而不是解釋模式, -XX:CompileCommand=print,*ReOrder.add1表示只列印這個方法,-XX:+PrintCompilation表示列印方法名稱。 需要外掛hsdis,編譯好後放在jdk的jre的bin的server中就好,具體環境搭建可以參閱這裡

分析不對的地方請輕拍。 訪問原文

相關文章