高級優化!編譯器優化你試過沒?分享給你一個完整的編譯器編譯規則的優化過程(附帶動手實驗)

全部的代碼轉化爲可執行文件,都須要經過編譯器將高級語言轉化爲計算器可以識別的低級語言。這個過程複雜且關鍵,很大程度上影響這門語言的性能。html

關於編譯器的優化工做也一直是人們研究的重點。可是,編譯的過程涉及的知識過多,不少時候咱們並不明白編譯的過程當中到底執行了什麼操做。git

本文經過分析Go編譯器優化的完整案例,向你們分享編譯器的編譯規則的優化方法。github

摘自OptimizeLab: https://github.com/OptimizeLab/docsgolang

做者:surechen數據庫


編譯器的做用是將高級語言的源代碼翻譯爲低級語言的目標代碼。一般爲了便於優化處理,編譯器會將源代碼轉換爲中間表示形式(Intermediate representation),不少編譯優化過程都是做用在這個形式上,以下面將介紹的經過給編譯器添加編譯規則優化性能。數組

在編譯Go語言代碼時一般使用Go語言編譯器,它包括語法分析AST變換靜態單賦值SSA PASS、機器碼生成等多個編譯過程。其中在生成SSA中間表示形式後進行了多個編譯優化過程PASS,每一個PASS都會對SSA形式的函數作轉換,好比deadcode elimination會檢測並刪除不會被執行的代碼和無用的變量。在全部PASS中lower會根據編寫好的優化規則將SSA中間表示從與體系結構(如X86ARM等)無關的轉換爲體系結構相關的,這是經過添加大量編譯規則實現的,是本文的主要關注點。瀏覽器

1. 浮點變量比較場景

浮點數在應用開發中有普遍的應用,如用來表示一個帶小數的金額或積分,常常會出現浮點數與0比較的狀況,如向數據庫錄入一個商品時,爲防止商品信息錯誤,能夠檢測錄入的金額是否大於0,當用戶購買產品時,可能須要先作一個驗證,檢測帳戶上金額是否大於0,若是知足再去查詢商品信息、錄入訂單等,這樣能夠在交易的開始階段排除一些無效或惡意的請求。性能優化

不少直播網站會舉行年度活動,經過榜單展示用戶活動期間累計送出禮物的金額,排名靠前的用戶會登上榜單。常常用浮點數表示累計金額,活動剛開始時,須要屏蔽掉積分小於等於0的條目,可能會用到以下函數:bash

func comp(x float64, arr []int) {
    for i := 0; i < len(arr); i++ {
        if x > 0 {
            arr[i] = 1
        }
    }
}

使用Go compile工具查看該函數的彙編代碼(爲便於理解,省略了部分無用代碼):服務器

go tool compile -S main.go
"".comp STEXT size=80 args=0x20 locals=0x0 leaf
        0x0000 00000 (main.go:3)        TEXT    "".comp(SB), LEAF|NOFRAME|ABIInternal, $0-32
#-------------------------將棧上數據取到寄存器中------------------------------
..................................
        0x0000 00000 (main.go:4)        MOVD    "".arr+16(FP), R0         // 取數組arr長度信息到寄存器R0中
..................................
        0x0004 00004 (main.go:4)        MOVD    "".arr+8(FP), R1           // 取數組arr地址值到寄存器R1中
        0x0008 00008 (main.go:4)        FMOVD   "".x(FP), F0                 // 將參數x放入F0寄存器
        0x000c 00012 (main.go:4)        MOVD    ZR, R2                          // ZR表示0,此處R2 清零
#---------------------------for循環執行邏輯----------------------------------
        0x0010 00016 (main.go:4)        JMP     24                                   // 第一輪循環直接跳到條件比較 不增長i
        0x0014 00020 (main.go:4)        ADD     $1, R2, R2                       // i++
        0x0018 00024 (main.go:4)        CMP     R0, R2                            // i < len(arr) 比較
        0x001c 00028 (main.go:4)        BGE     68                                   // i == len(arr) 跳轉到末尾
#--------if x > 0---------
        0x0020 00032 (main.go:5)        FMOVD   ZR, F1                         // 將0複製到浮點寄存器F1
        0x0024 00036 (main.go:5)        FCMPD   F1, F0                          // 將浮點寄存器F0和F1中的值進行比較
#--------arr[i] = 1-------
        0x0028 00040 (main.go:5)        CSET    GT, R3                            // 若是F0 > F1 : R3 = 1
        0x002c 00044 (main.go:5)        CBZ     R3, 60                             // R3 == 1 即 x <= 0 跳轉到60
        0x0030 00048 (main.go:6)        MOVD    $1, R3                         // x > 0
        0x0034 00052 (main.go:6)        MOVD    R3, (R1)(R2<<3)         // 將切片中值賦值爲1
        0x0038 00056 (main.go:6)        JMP     20                                  // 跳轉到20 即循環操做i++處
#--------x <= 0 跳到i++----
        0x003c 00060 (main.go:6)        MOVD    $1, R3                         // x <= 0
        0x0040 00064 (main.go:5)        JMP     20
...........................................................................
...........................................................................

能夠看到對於浮點數與0的比較,上述代碼首先將0放入F1寄存器,以後使用FCMPD命令將F0寄存器中的變量值x與F1寄存器中的0值進行比較:

這裏對彙編性能優化有必定基礎的讀者可能會產生疑問,爲何一個浮點變量與常數0的比較要都放入寄存器才能進行,這裏須要瞭解ARMV8的浮點數比較指令FCMP,它有兩種用法:

  1. 將兩個浮點寄存器中的值進行比較;
  2. 將一個浮點寄存器中的值與數值0比較;

能夠看到對於FCMP指令,雖然浮點數與幾乎全部常量比較都必須先放入寄存器中,但與0比較是一個特例,不須要將0放入一個浮點寄存器中,能夠直接使用FCMP F0, $(0) 進行比較,所以上述生成的彙編代碼並非最優的

2. 優化編譯規則提高浮點變量比較性能

看起來是個不復雜但大量出現的問題,編譯器卻作不到最優化,讓代碼愛好者倍感失望,怎麼解決呢?下面是Go語言社區浮點值變量與0比較的編譯規則優化案例,它經過簡單地增長編譯規則給編譯器賦能:

image

優化後全部的浮點值變量在與0的比較運算中都會受益。爲便於讀者直觀的看到具體的SSA中間表示和優化先後的變化,下面經過Go編譯器工具查看詳細的編譯過程,編譯器會將SSA PASS的詳細過程記錄到一個ssa.html文件,使用瀏覽器打開後可以直觀的看到每一個SSA PASS對中間表示形式的修改,先看下編譯規則優化前的效果圖:

image

SSA PASS過程不少,主要關注最後一幅圖,它是SSA PASS執行完的最終形式,注意圖中v24和v20處,優化前將常量0放入寄存器F1中,將數組元素放入寄存器F0中,而後纔會調用FCMPD浮點比較指令比較F1和F0中的值,並根據比較結果更新狀態寄存器:

image

如今問題已經很清晰了,優化的目的就是指望將兩條指令:FMOVD $(0.0), F1 和 FCMPD F1, F0 轉變爲一條指令 FCMPD $(0.0), F0

在這個優化中讓編譯器更智能的技術就是以下的SSA編譯規則,採用S-表達式形式,它的做用就是找到匹配的表達式並轉換爲編譯器指望的另外一種效率更高或體系結構相關的表達式,以下圖所示:

image

上述優化中讓編譯器變得更聰明是由於他新學習到了下面的轉換規則,初步理解編譯規則語法後不難理解:

// 將浮點數與0比較優化爲表達式"FCMP $(0.0), Fn"
(FCMPS x (FMOVSconst [0])) -> (FCMPS0 x)                              // 32位浮點數x與常數0比較 -> FCMPS0 x
(FCMPS (FMOVSconst [0]) x) -> (InvertFlags (FCMPS0 x))         // 常數0與32位浮點數x比較 -> (FCMPS0 x) 結果取反
(FCMPD x (FMOVDconst [0])) -> (FCMPD0 x)                            // 64位浮點數x與常數0比較 -> FCMPD0 x
(FCMPD (FMOVDconst [0]) x) -> (InvertFlags (FCMPD0 x))       // 常數0與64位浮點數x比較 -> (FCMPD0 x) 結果取反
-------------------比較結果取反規則-----------------------
(LessThanF (InvertFlags x)) -> (GreaterThanF x)                        // 取反(a < b) -> a > b
(LessEqualF (InvertFlags x)) -> (GreaterEqualF x)                      // 取反(a <= b) -> a >= b
(GreaterThanF (InvertFlags x)) -> (LessThanF x)                        // 取反(a > b) -> a < b
(GreaterEqualF (InvertFlags x)) -> (LessEqualF x)                      // 取反(a >= b) -> a <= b

注:因爲不涉及,爲便於理解,在上述例子中忽略了 <type>-類型、[auxint]-變量值、{aux}-非數值變量值、 [&& extra conditions]:-條件表達式等經常使用的表達式類型字段,感興趣的讀者能夠根據上文列舉的資料進一步探究

細緻的讀者可能已經意識到另外一個問題了,規則裏面有兩個精簡的操做碼FCMPS0和FCMPD0,他們爲何更優呢?首先根據名字也許已經猜到他們分別表示單精度浮點數(32bit)與0比較和雙精度浮點數(64bit)與0比較,具體含義以下圖所示:

image

如今讀者已經瞭解了編譯器SSA規則優化的各個組成部分,整理一下思路,將各部分串聯起來能夠畫出以下精簡的架構圖,在Go編譯器中編譯規則優化是SSA PASS的重要組成部分,他幫助編譯器將一些體系結構無關的通用表達式轉換爲更高效的表達式,如對於冗餘的條件判斷取反表達式,去掉取反操做,直接對判斷條件取反,如invert(<=)轉變爲>,體系結構無關表達式轉爲與體系結構(ARM6四、X86等)相關的表達式:

image

增長上述編譯規則,將編譯器更新到最新版,再生成SSA PASS執行結果圖,能夠看到最終兩條指令變成了一條:

image

3. 代碼詳解

下面是優化代碼詳解:

  1. 操做碼定義,這裏增長了兩個新的浮點數與0比較操做碼:
//--------------定義一個寄存器的輸入參數掩碼,此處fp表示全部浮點數寄存器----------------
fp1flags  = regInfo{inputs: []regMask{fp}}
..............................
//--------------------------增長兩個浮點數與0比較的操做碼----------------------------
// 定義操做FCMPS0,將浮點寄存器中的參數(float32)與0進行比較,使用匯編指令FCMPS
{name: "FCMPS0", argLength: 1, reg: fp1flags, asm: "FCMPS", typ: "Flags"},
// 定義操做FCMPD0,將浮點寄存器中的參數(float64)與0進行比較,使用匯編指令FCMPD
{name: "FCMPD0", argLength: 1, reg: fp1flags, asm: "FCMPD", typ: "Flags"},
  1. 根據操做碼自動生成opGen.go:
{
    name:   "FCMPS0",                       // 操做名
    argLen: 1,                                     // 參數個數
    asm:    arm64.AFCMPS,                 // 對應的機器指令,此處爲ARM64平臺的FCMPS
    reg: regInfo{
        inputs: []inputInfo{                  // 支持的輸入參數寄存器
            {0, 9223372034707292160},  // F0 F1 F2 F3 F4 F5 F6 F7 F8 F9 F10 F11 F12 F13 F14 F15 F16 F17 F18 F19 F20 F21 F22 F23 F24 F25 F26 F27 F28 F29 F30 F31
        },
    },
},
{
    name:   "FCMPD0",                       // 操做名
    argLen: 1,                                     // 參數個數
    asm:    arm64.AFCMPD,                 // 對應的機器指令,此處爲ARM64平臺的FCMPD
    reg: regInfo{
        inputs: []inputInfo{                  // 支持的輸入參數寄存器
            {0, 9223372034707292160},  // F0 F1 F2 F3 F4 F5 F6 F7 F8 F9 F10 F11 F12 F13 F14 F15 F16 F17 F18 F19 F20 F21 F22 F23 F24 F25 F26 F27 F28 F29 F30 F31
        },
    },
},
  1. 操做碼FCMPS0和FCMPD0轉爲prog形式,每一個prog對應具體的一條指令,連接時會用到。
case ssa.OpARM64FCMPS0,                          // FCMPS0 -> FCMPS $(0.0), F0
        ssa.OpARM64FCMPD0:                            // FCMPD0 -> FCMPD $(0.0), F0
        p := s.Prog(v.Op.Asm())                          // FCMPS | FCMPD
        p.From.Type = obj.TYPE_FCONST             // $(0.0) 的類型爲常數
        p.From.Val = math.Float64frombits(0)    // 比較的數 $(0.0)
        p.Reg = v.Args[0].Reg()                           // 第二個源操做數,即用於比較的浮點數寄存器F0
  1. 在ARM64.rules中加入新的SSA編譯規則
// 將浮點數與0比較優化爲表達式"FCMP $(0.0), Fn"
(FCMPS x (FMOVSconst [0])) -> (FCMPS0 x)                               // 32位浮點數x與常數0比較 -> FCMPS0 x
(FCMPS (FMOVSconst [0]) x) -> (InvertFlags (FCMPS0 x))          // 常數0與32位浮點數x比較 -> (FCMPS0 x) 結果取反
(FCMPD x (FMOVDconst [0])) -> (FCMPD0 x)                             // 64位浮點數x與常數0比較 -> FCMPD0 x
(FCMPD (FMOVDconst [0]) x) -> (InvertFlags (FCMPD0 x))        // 常數0與64位浮點數x比較 -> (FCMPD0 x) 結果取反
-------------------比較結果取反規則-----------------------
(LessThanF (InvertFlags x)) -> (GreaterThanF x)                        // 取反(a < b) -> a > b
(LessEqualF (InvertFlags x)) -> (GreaterEqualF x)                      // 取反(a <= b) -> a >= b
(GreaterThanF (InvertFlags x)) -> (LessThanF x)                        // 取反(a > b) -> a < b
(GreaterEqualF (InvertFlags x)) -> (LessEqualF x)                      // 取反(a >= b) -> a <= b
  1. 根據ARM64.rules自動生成的Go轉換代碼:
//--------------在lower pass中如下規則會挨個進行匹配,匹配後執行轉換----------------
case OpARM64FCMPD:
    return rewriteValueARM64_OpARM64FCMPD_0(v)
case OpARM64FCMPS:
    return rewriteValueARM64_OpARM64FCMPS_0(v)
case OpARM64GreaterEqualF:
    return rewriteValueARM64_OpARM64GreaterEqualF_0(v)
case OpARM64GreaterThanF:
    return rewriteValueARM64_OpARM64GreaterThanF_0(v)
case OpARM64LessEqualF:
    return rewriteValueARM64_OpARM64LessEqualF_0(v)
case OpARM64LessThanF:
    return rewriteValueARM64_OpARM64LessThanF_0(v)

//------------------------x(float64)與0比較 轉爲 FCMPD0 x------------------------
func rewriteValueARM64_OpARM64FCMPD_0(v *Value) bool {
    b := v.Block
    _ = b
    // match: (FCMPD x (FMOVDconst [0]))
    // cond:
    // result: (FCMPD0 x)
    for {
        _ = v.Args[1]
        x := v.Args[0]
        v_1 := v.Args[1]
        if v_1.Op != OpARM64FMOVDconst {
            break
        }
        if v_1.AuxInt != 0 {                                                                   // 若是不是與0比較,則退出
            break
        }
        v.reset(OpARM64FCMPD0)                                                      // 修改OpARM64FCMPD指令爲OpARM64FCMPD0
        v.AddArg(x)
        return true
    }
    // match: (FCMPD (FMOVDconst [0]) x)
    // cond:
    // result: (InvertFlags (FCMPD0 x))
    for {
        _ = v.Args[1]
        v_0 := v.Args[0]
        if v_0.Op != OpARM64FMOVDconst {
            break
        }
        if v_0.AuxInt != 0 {                                                                    // 若是不是與0比較,則退出
            break
        }
        x := v.Args[1]
        v.reset(OpARM64InvertFlags)                                                   // 修改OpARM64FCMPD指令爲OpARM64InvertFlags
        v0 := b.NewValue0(v.Pos, OpARM64FCMPD0, types.TypeFlags) // 添加一個表示OpARM64FCMPD0指令的value(SSA表示一個值)
        v0.AddArg(x)
        v.AddArg(v0)
        return true
    }
    return false
}

//------------------------x(float32)與0比較 轉爲 FCMPS0 x------------------------
func rewriteValueARM64_OpARM64FCMPS_0(v *Value) bool {
    b := v.Block
    _ = b
    // match: (FCMPS x (FMOVSconst [0]))
    // cond:
    // result: (FCMPS0 x)
    for {
        _ = v.Args[1]
        x := v.Args[0]
        v_1 := v.Args[1]
        if v_1.Op != OpARM64FMOVSconst {
            break
        }
        if v_1.AuxInt != 0 {                              // 若是操做數不爲0,退出
            break
        }
        v.reset(OpARM64FCMPS0)                  // 修改OpARM64FCMPS指令爲OpARM64FCMPS0
        v.AddArg(x)
        return true
    }
    // match: (FCMPS (FMOVSconst [0]) x)
    // cond:
    // result: (InvertFlags (FCMPS0 x))
    for {
        _ = v.Args[1]
        v_0 := v.Args[0]
        if v_0.Op != OpARM64FMOVSconst {
            break
        }
        if v_0.AuxInt != 0 {
            break
        }
        x := v.Args[1]
        v.reset(OpARM64InvertFlags)
        v0 := b.NewValue0(v.Pos, OpARM64FCMPS0, types.TypeFlags)
        v0.AddArg(x)
        v.AddArg(v0)
        return true
    }
    return false
}

//--------------帶反轉標誌的浮點數比較:invert(x >= 0) 轉爲 x <= 0----------------
func rewriteValueARM64_OpARM64GreaterEqualF_0(v *Value) bool {
    // match: (GreaterEqualF (InvertFlags x))
    // cond:
    // result: (LessEqualF x)
    for {
        v_0 := v.Args[0]
        if v_0.Op != OpARM64InvertFlags { // 不需反轉,此轉換規則不適用,退出繼續後面的規則
            break
        }
        x := v_0.Args[0]
        v.reset(OpARM64LessEqualF)         // 修改OpARM64GreaterEqualF指令爲OpARM64LessEqualF
        v.AddArg(x)
        return true
    }
    return false
}

//--------------帶反轉標誌的浮點數比較操做:invert(x > 0) 轉換 x < 0-----------------
func rewriteValueARM64_OpARM64GreaterThanF_0(v *Value) bool {
    // match: (GreaterThanF (InvertFlags x))
    // cond:
    // result: (LessThanF x)
    for {
        v_0 := v.Args[0]
        if v_0.Op != OpARM64InvertFlags { // 不需反轉,此轉換規則不適用,退出繼續後面的規則
            break
        }
        x := v_0.Args[0]
        v.reset(OpARM64LessThanF)          // 修改OpARM64GreaterThanF指令爲OpARM64LessThanF
        v.AddArg(x)
        return true
    }
    return false
}

//-------------帶反轉標誌的浮點數比較操做:invert(x <= 0) 轉爲 x >= 0----------------
func rewriteValueARM64_OpARM64LessEqualF_0(v *Value) bool {
    // match: (LessEqualF (InvertFlags x))
    // cond:
    // result: (GreaterEqualF x)
    for {
        v_0 := v.Args[0]
        if v_0.Op != OpARM64InvertFlags { // 不需反轉,此轉換規則不適用,退出繼續後面的規則
            break
        }
        x := v_0.Args[0]
        v.reset(OpARM64GreaterEqualF)    // 修改OpARM64LessEqualF指令爲OpARM64GreaterEqualF
        v.AddArg(x)
        return true
    }
    return false
}

//-------------帶反轉標誌的浮點數比較操做:invert(x < 0) 轉爲 x > 0----------------
func rewriteValueARM64_OpARM64LessThanF_0(v *Value) bool {
    // match: (LessThanF (InvertFlags x))
    // cond:
    // result: (GreaterThanF x)
    for {
        v_0 := v.Args[0]
        if v_0.Op != OpARM64InvertFlags { // 不需反轉,此轉換規則不適用,退出繼續後面的規則
            break
        }
        x := v_0.Args[0]
        v.reset(OpARM64GreaterThanF)     // 替換OpARM64LessThanF指令爲OpARM64GreaterThanF
        v.AddArg(x)
        return true
    }
    return false
}

4. 動手實驗

感興趣的讀者能夠按照本章本身動手執行一遍,體驗編譯規則優化如何幫助編譯器變得更聰明:

  • 環境準備
  1. 硬件配置:鯤鵬(ARM64)雲Linux服務器-通用計算加強型KC1 kc1.2xlarge.2(8核|16GB)
  2. Go語言發行版 1.12.1 — 1.12.17,此處開發環境準備請參考文章:Go在ARM64開發環境配置
  3. Go語言github源碼倉庫下載,此處經過Git安裝和使用進行版本控制。
  4. 測試代碼
  5. 編譯規則代碼生成工具
  • 操做步驟
# 準備一個測試目錄如/usr/local/src/
cd /usr/local/src
# 拉取測試用例代碼
git clone https://github.com/OptimizeLab/sample
# 進入compile/ssa/opt_float_cmp_0_by_SSA_rule/src
cd /usr/local/src/sample/compile/ssa/opt_float_cmp_0_by_SSA_rule/src
# Go語言發行版1.12沒有包含這個優化的編譯規則,所以直接使用發行版自帶的Go編譯器
# 獲取並查看優化前的ssa.html
GOSSAFUNC=comp go tool compile main.go
# 使用Go benchmark命令測試性能並記錄在文件before-ssa-bench.txt中
go test -bench BenchmarkFloatCompare -count=5 > before-ssa-bench.txt
# 接下來使用優化後的Go編譯器獲取ssa.html
# 找到一個放置Go源碼倉的目錄,如/usr/local/src/exp
mkdir /usr/local/src/exp
cd /usr/local/src/exp
# 經過git工具拉取github代碼託管平臺上golang的代碼倉
git clone https://github.com/golang/go
# 拉取的最新源碼已經包含了這個優化,所以能夠直接編譯得到最新的Go編譯器
# 進入源碼目錄
cd /usr/local/src/exp/go/src
# 編譯Go源碼,生成Go開發環境
bash ./make.bash
# 切換回測試代碼目錄
cd /usr/local/src/sample/compile/ssa/opt_float_cmp_0_by_SSA_rule/src
# 指定GOROOT目錄,GOSSAFUNC關鍵字選擇要展現的函數,本文中是comp,生成優化後的ssa.html
GOROOT=/usr/local/src/exp/go; GOSSAFUNC=comp go tool compile main.go
# 使用Go benchmark命令測試性能並記錄在文件after-ssa-bench.txt中
GOROOT=/usr/local/src/exp/go; go test -bench BenchmarkFloatCompare -count=5 > after-ssa-bench.txt
# benchstat 對比結果
benchstat before-ssa-bench.txt after-ssa-bench.txt
# 值得一提的是開發新的編譯規則時,只須要編寫.rules和Ops.go文件,並經過[編譯規則代碼生成工具]生成規則轉換執行代碼
# 自動生成的文件包括opGen.go 和 rewriteARM64.go
cd /usr/local/src/exp/go/src/cmd/compile/internal/ssa/gen
go run *.go

5. 結論

上述優化案例最終使ARM64平臺上全部浮點變量與0比較的性能都獲得優化,雖然提高較小,但因爲其能在用戶無需任何改動的狀況下使全部符合上述規則的海量代碼獲得優化,所以是一個很是有價值的優化。

若是對開源或優化技術感興趣,歡迎下方留言或者經過https://github.com/OptimizeLab/docs/issues聯繫咱們。

相關文章
相關標籤/搜索