Go 性能分析之案例一

思考

相信你們在實際的項目開發中會遇到這麼一個事,有的程序員寫的代碼不只bug少,並且性能高;而有的程序員寫的代碼可否流暢的跑起來,都是一個很大問題。
而咱們今天要討論的就是一個關於性能優化的案例分析。css

案例分析

咱們先來構造一些基礎數據(長度爲10億的切片,並賦上值):程序員

var testData = GenerateData()

// generate billion slice data
func GenerateData() []int {
    data := make([]int, 1000000000)
    for key, _ := range data {
        data[key] = key % 128
    }

    return data
}

// get length
func GetDataLen() int {
    return len(testData)
}

 

案例一

// case one
func CaseSumOne(result *int) {
    data := GenerateData()
    for i := 0; i < GetDataLen(); i++ {
        *result += data[i]
    }
}
// case two
func CaseSumTwo(result *int) {
    data := GenerateData()
    dataLen := GetDataLen()
    for i := 0; i < dataLen; i++ {
        *result += data[i]
    }
}

 

執行結果

$ go test -bench=.
goos: windows
goarch: amd64
BenchmarkCaseSumOne-8                  1        7439749000 ns/op
BenchmarkCaseSumTwo-8                  1        2529266700 ns/op
PASS
ok      _/C_/go-code/perform/case-one   14.059s

 

問題分析

  • CaseSumTwo執行效率是CaseSumOne的2.94倍,快了近三倍,這是爲何呢?
  • 我想這個其實很容易猜到,這裏有一個連續的函數調用「GetDataLen()」,

咱們來看下兩個函數的彙編,作個簡單的對比:算法

函數CaseSumOne

"".CaseSumOne STEXT size=83 args=0x4 locals=0xc
        0x0000 00000 (point.go:22)      TEXT    "".CaseSumOne(SB), $12-4
        ...
        // point.go:24 -> for i := 0; i < GetDataLen(); i++ 
        0x0021 00033 (point.go:24)      PCDATA  $2, $2
        0x0021 00033 (point.go:24)      PCDATA  $0, $1
        0x0021 00033 (point.go:24)      MOVL    "".result+16(SP), DX 
        0x0025 00037 (point.go:24)      XORL    BX, BX
        0x0027 00039 (point.go:24)      JMP     47
        0x0029 00041 (point.go:25)      MOVL    (CX)(BX*4), BP    // CX循環計數器
        0x002c 00044 (point.go:25)      ADDL    BP, (DX)
        0x002e 00046 (point.go:24)      INCL    BX // i++
        0x002f 00047 (point.go:24)      MOVL    "".testData+4(SB), BP // 棧指針寄存器
        0x0035 00053 (point.go:24)      CMPL    BX, BP
        0x0037 00055 (point.go:24)      JGE     65
        ...
        0x0045 00069 (point.go:25)      CALL    runtime.panicindex(SB)
        0x004c 00076 (point.go:22)      CALL    runtime.morestack_noctxt(SB)
        ...

 

函數CaseSumTwo

"".CaseSumTwo STEXT size=83 args=0x4 locals=0xc
        0x0000 00000 (point.go:30)      TEXT    "".CaseSumTwo(SB), $12-4
        ...
        // point.go:32 -> dataLen := GetDataLen()
        // point.go:33 -> for i := 0; i < dataLen; i++ {
        0x0021 00033 (point.go:32)      MOVL    "".testData+4(SB), DX
        0x0027 00039 (point.go:33)      PCDATA  $2, $2
        0x0027 00039 (point.go:33)      PCDATA  $0, $1
        0x0027 00039 (point.go:33)      MOVL    "".result+16(SP), BX
        0x002b 00043 (point.go:33)      XORL    BP, BP
        0x002d 00045 (point.go:33)      JMP     53
        0x002f 00047 (point.go:34)      MOVL    (AX)(BP*4), SI
        0x0032 00050 (point.go:34)      ADDL    SI, (BX)
        0x0034 00052 (point.go:33)      INCL    BP
        0x0035 00053 (point.go:33)      CMPL    BP, DX
        0x0037 00055 (point.go:33)      JGE     65
        ...
        0x0045 00069 (point.go:34)      CALL    runtime.panicindex(SB)
        0x004c 00076 (point.go:30)      CALL    runtime.morestack_noctxt(SB)
        ...

 

比較結論

不難發現主要的區別是在CaseSumOne中多了這麼一行:windows

0x002f 00047 (point.go:24) MOVL "".testData+4(SB), BP

其實雖然只有一行,可是對於函數「GetDataLen」裏須要調用的指令對CPU的消耗:性能優化

"".GetDataLen STEXT size=36 args=0x4 locals=0x0
        0x0000 00000 (point.go:17)      TEXT    "".GetDataLen(SB), $0-4 // 
        0x0000 00000 (point.go:17)      MOVL    TLS, CX
        0x0007 00007 (point.go:17)      MOVL    (CX)(TLS*2), CX
        0x000d 00013 (point.go:17)      CMPL    SP, 8(CX)
        0x0010 00016 (point.go:17)      JLS     29
        0x0012 00018 (point.go:17)      FUNCDATA        $0, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
        0x0012 00018 (point.go:17)      FUNCDATA        $1, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
        0x0012 00018 (point.go:17)      FUNCDATA        $3, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
        0x0012 00018 (point.go:18)      PCDATA  $2, $0
        0x0012 00018 (point.go:18)      PCDATA  $0, $0
        0x0012 00018 (point.go:18)      MOVL    "".testData+4(SB), AX // 寄存器尋址 AX = lenVAL
        0x0018 00024 (point.go:18)      MOVL    AX, "".~r0+4(SP)    // SP = AX = lenVal
        0x001c 00028 (point.go:18)      RET
        0x001d 00029 (point.go:18)      NOP
        0x001d 00029 (point.go:17)      PCDATA  $0, $-1
        0x001d 00029 (point.go:17)      PCDATA  $2, $-1
        0x001d 00029 (point.go:17)      CALL    runtime.morestack_noctxt(SB)    // 壓棧
        ...

 

雖然,看似小小一行代碼的區別,可是在指令級的角度上,進行了建立棧空間、壓棧、尋址、賦值等一系列操做,何況這裏進行了循環調用。數據結構

案例二

// case two
func CaseSumTwo(result *int) {
    data := GenerateData()
    dataLen := GetDataLen()
    for i := 0; i < dataLen; i++ {
        *result += data[i]
    }
}
// case three
func CaseSumThree(result *int) {
    data := GenerateData()
    dataLen := GetDataLen()
    tmp := *result
    for i:= 0; i < dataLen; i++ {
        tmp += data[i]
    }
    *result = tmp
}

 

執行結果

$ go test -bench=.
goos: windows
goarch: amd64
BenchmarkCaseSumTwo-8                  1        2529266700 ns/op
BenchmarkCaseSumThree-8                1        1657554600 ns/op
PASS
ok      _/C_/go-code/perform/case-one   8.2773

 

問題分析

  • 雖然對連續函數調用進行了優化,可是CaseSumThree對執行效率仍是高於CaseSumTwo1.52倍,還有哪些狀況會影響執行性能呢?

咱們再來對比下「CaseSumTwo」和「CaseSumThree」對彙編源碼:函數

函數CaseSumTwo

"".CaseSumTwo STEXT size=83 args=0x4 locals=0xc
        0x0000 00000 (point.go:30)      TEXT    "".CaseSumTwo(SB), $12-4
        ...
        // point.go:31 -> data := GenerateData()
        // point.go:34 -> *result += data[i] 
        0x001a 00026 (point.go:31)      MOVL    (SP), AX
        0x0027 00039 (point.go:33)      MOVL    "".result+16(SP), BX
        0x002f 00047 (point.go:34)      MOVL    (AX)(BP*4), SI // 棧寄存器移動四個字節, -> SI源變址寄存器
        0x0032 00050 (point.go:34)      ADDL    SI, (BX)  // SI
        0x0034 00052 (point.go:33)      INCL    BP
        0x0035 00053 (point.go:33)      CMPL    BP, DX
        0x0037 00055 (point.go:33)      JGE     65
        0x0039 00057 (point.go:34)      TESTB   AX, (BX)
        0x003b 00059 (point.go:34)      CMPL    BP, CX
        0x003d 00061 (point.go:34)      JCS     47
        0x003f 00063 (point.go:34)      JMP     69
        0x0041 00065 (<unknown line number>)    PCDATA  $2, $-2
        0x0041 00065 (<unknown line number>)    PCDATA  $0, $-2
        0x0041 00065 (<unknown line number>)    ADDL    $12, SP
        0x0044 00068 (<unknown line number>)    RET
        0x0045 00069 (point.go:34)      PCDATA  $2, $0
        0x0045 00069 (point.go:34)      PCDATA  $0, $1
        0x0045 00069 (point.go:34)      CALL    runtime.panicindex(SB)
        0x004a 00074 (point.go:34)      UNDEF
        0x004c 00076 (point.go:34)      NOP

 

函數CaseSumThree

"".CaseSumThree STEXT size=97 args=0x4 locals=0x10
        0x0000 00000 (point.go:39)      TEXT    "".CaseSumThree(SB), $16-4
        ...
        // point.go:40 -> data := GenerateData()
        // point.go:42 -> tmp := *result
        // point.go:44 -> tmp += data[i]
        // point.go:46 -> *result = tmp
        0x001a 00026 (point.go:40)      MOVL    (SP), AX
        0x0021 00033 (point.go:42)      PCDATA  $2, $2
        0x0021 00033 (point.go:42)      PCDATA  $0, $1
        0x0021 00033 (point.go:42)      MOVL    "".result+20(SP), DX
        0x0025 00037 (point.go:42)      MOVL    (DX), BX // ->BX數據指針寄存器
        0x0027 00039 (point.go:41)      MOVL    "".testData+4(SB), BP
        0x002d 00045 (point.go:41)      XORL    SI, SI
        0x002f 00047 (point.go:43)      JMP     67
        0x0031 00049 (point.go:43)      LEAL    1(SI), DI
        0x0034 00052 (point.go:43)      MOVL    DI, "".i+12(SP) // 移動DI到棧指針12字節的位置
        0x0038 00056 (point.go:44)      MOVL    (AX)(SI*4), DI // 源變址寄存器移動四個字節(32位),-> 目的變址寄存器
        0x003b 00059 (point.go:44)      ADDL    DI, BX // DI+BX
        0x003d 00061 (point.go:43)      MOVL    "".i+12(SP), DI 
        0x0041 00065 (point.go:43)      MOVL    DI, SI
        0x0043 00067 (point.go:43)      CMPL    SI, BP
        0x0045 00069 (point.go:43)      JGE     77
        0x0047 00071 (point.go:44)      CMPL    SI, CX
        0x0049 00073 (point.go:44)      JCS     49
        0x004b 00075 (point.go:44)      JMP     83
        0x004d 00077 (point.go:46)      PCDATA  $2, $0
        0x004d 00077 (point.go:46)      MOVL    BX, (DX)
        0x004f 00079 (point.go:47)      ADDL    $16, SP
        0x0052 00082 (point.go:47)      RET
        0x0053 00083 (point.go:44)      CALL    runtime.panicindex(SB)
        ...

 

比較結論

CaseSumTwo函數,在進行ADDL以前,由於「*result」爲指針變量,因此不能直接與data[i]運算。所以須要建立一個棧空間,並指向data的地址並,而後經過移動棧指針後獲得下一個值的地址,並賦與SI。
CaseSumThree函數,在進行ADDL執行前,建立了一個值變量,那麼在執行ADDL的時候,只須要移動SI獲取下一個data的值就能夠直接進行算數運算,中間少了地址的引用的棧的操做。性能

總結

本章主要講了三個點:優化

  1. 消除循環的低效率
  2. 減小過程調用
  3. 消除沒必要要的內存引用

引用《深刻計算機系統原理》一書中對性能優化所提到的三個方面:編碼

  1. 高級設計,爲遇到的問題選擇適當的算法和數據結構。要特別警覺,避免使用那些會漸進地產生糟糕性能的算法或編碼技術。
  2. 基本編碼原則,從指令的角度考慮,開發中應如何編碼,才能減小執行的指令。
  3. 低級優化,針對現代處理器,如何讓cpu的流水線儘可能飽合。

因此,一個優秀的程序員在寫每一行代碼,定義每個變量,也許背後思考的就會更多。

相關文章
相關標籤/搜索