[翻譯]Go 和彙編

來源:互聯網
上載者:User
這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。

使用 cgo 讓 Go 跟 C 一起工作已經不是啥稀奇的了。有大量的第三方包直接對 C 的庫做了封裝,提供給 Go 使用。從 Go 項目本身的代碼中可以看到,不但有 C 代碼,還有彙編代碼存在。那麼在自己的項目中是否能跟彙編結合呢?這篇文章完整並清晰的解說了如何讓 Go 和彙編協同工作。真得效能敏感?上彙編吧!!

————翻譯分隔線————

Go 和彙編

關於 Go,我最喜歡的部分之一就是它那堅定不移的實用主義線路。有時我們過於強調語言的設計,而忘記了編程所包含的其他內容。例如:

  • Go 的編譯器很快
  • Go 有著強大的標準庫
  • Go 可以工作在多種平台下
  • Go 有著可以通過命令列/本地 Web 服務/ 網際網路存取的完整文檔
  • 所有 Go 的代碼是靜態編譯的,因此部署的問題微不足道
  • 全部 Go 的代碼都以良好的格式發布,可以線上閱讀(就像這個)
  • Go 有著良好定義(和文檔)的文法。(不像 C++ 或 Ruby)
  • Go 內建包管理工具。go get X(例如 go get code.google.com/p/go.net/websocket
  • 跟其他語言一樣,Go 有編碼樣式指引,有一些是編譯器強制的(例如大寫和小寫),而其他一些僅僅是約定,不過它還是提供了整理代碼的工具:gofmt name_of_file.go
  • 還有工具 go fix 可以將 Go 代碼從早版本自動遷移到新的版本
  • Go 帶有測試載入器來測試包:go test /path/to/package。它還可以進行效能評估。
  • 可以調試 和評估 Go 程式。
  • 你知道有個遊樂場可以線上嘗試 Go 嗎?
  • 通過 cgo Go 可以整合 C 的庫。

這些都已經有一些例子了,不過這裡我想聚焦在一個不怎麼為人所知的話題:Go 可以無縫調用彙編編寫的函數

如何在 Go 中使用彙編

假設我們需要編寫一個彙編版本的 sum 函數。首先建立一個叫做 sum.go 的檔案,內容如下:

package sumfunc Sum(xs []int64) int64 {  var n int64  for _, v := range xs {    n += v  }  return n}

這個函數將一個整型的 slice 相加,並返回結果。為了測試這個函數,建立一個叫做 sum_test.go 的檔案,內容如下:

package sumimport (  "testing")type (  testCase struct {    n int64    xs []int64  })var (  cases = []testCase{    { 0, []int64{} },    { 15, []int64{1,2,3,4,5} },  })func TestSum(t *testing.T) {  for _, tc := range cases {    n := Sum(tc.xs)    if tc.n != n {      t.Error("Expected", tc.n, "got", n, "for", tc.xs)    }  }}

為你的代碼編寫測試是個不錯的主意,不但可以檢驗庫的代碼(只要不是 package main|譯註:package main 中的方法也是可以使用 go test 進行測試的),還是一個用於實驗的好方法。在命令列輸入 go test 就可以運行這個測試。

現在讓我們用彙編來代替這個函數。我們可以來看看 Go 編譯器到底產生了什麼。用命令 go tool 6g -S sum.go 來代替 go test 或者 go build(對於 64 位元來說)。你會得到下面的內容:

--- prog list "Sum" ---0000 (sum.go:3) TEXT    Sum+0(SB),$16-240001 (sum.go:4) MOVQ    $0,SI0002 (sum.go:5) MOVQ    xs+0(FP),BX0003 (sum.go:5) MOVQ    BX,autotmp_0000+-16(SP)0004 (sum.go:5) MOVL    xs+8(FP),BX0005 (sum.go:5) MOVL    BX,autotmp_0000+-8(SP)0006 (sum.go:5) MOVL    xs+12(FP),BX0007 (sum.go:5) MOVL    BX,autotmp_0000+-4(SP)0008 (sum.go:5) MOVL    $0,AX0009 (sum.go:5) MOVL    autotmp_0000+-8(SP),DI0010 (sum.go:5) LEAQ    autotmp_0000+-16(SP),BX0011 (sum.go:5) MOVQ    (BX),CX0012 (sum.go:5) JMP     ,140013 (sum.go:5) INCL    ,AX0014 (sum.go:5) CMPL    AX,DI0015 (sum.go:5) JGE     ,200016 (sum.go:5) MOVQ    (CX),BP0017 (sum.go:5) ADDQ    $8,CX0018 (sum.go:6) ADDQ    BP,SI0019 (sum.go:5) JMP     ,130020 (sum.go:8) MOVQ    SI,.noname+16(FP)0021 (sum.go:8) RET     ,sum.go:3: Sum xs does not escape

彙編是相當難理解的,一會我們會詳細瞭解一下這個部分……不過,首先用這個作為模板接著往下做。在 sum.go 同一目錄建立一個叫做 sum_amd64.s 的檔案,內容如下:

// func Sum(xs []int64) int64TEXT ·Sum(SB),$0    MOVQ    $0,SI    MOVQ    xs+0(FP),BX    MOVQ    BX,autotmp_0000+-16(SP)    MOVL    xs+8(FP),BX    MOVL    BX,autotmp_0000+-8(SP)    MOVL    xs+12(FP),BX    MOVL    BX,autotmp_0000+-4(SP)    MOVL    $0,AX    MOVL    autotmp_0000+-8(SP),DI    LEAQ    autotmp_0000+-16(SP),BX    MOVQ    (BX),CX    JMP     L2L1: INCL    AXL2: CMPL    AX,DI    JGE     L3    MOVQ    (CX),BP    ADDQ    $8,CX    ADDQ    BP,SI    JMP     L1L3: MOVQ    SI,.noname+16(FP)    RET

基本上,我所做的所有處理就是將硬式編碼用於跳轉(JMP,JGE)的行號替換為標籤,並且在函數名前增加了中點符(·)。(確保檔案儲存為 UTF-8 編碼)接下來,從 sum.go 中移除我們的函數定義:

package sumfunc Sum(xs []int64) int64

現在,應當可以用 go test 運行測試,它將使用自訂的彙編版本的函數。

工作原理

這裡對彙編做一些更為詳細的說明。我將簡短的說明一下它做了什麼。

MOVQ    $0,SI

首先,將 0 放入 SI(源變址)寄存器,它表示執行的指令的位置。Q 表示四個字,8 位元,下面還會看到 L 表示 4 位元。參數的順序是(源,目標)。

MOVQ    xs+0(FP),BXMOVQ    BX,autotmp_0000+-16(SP)MOVL    xs+8(FP),BXMOVL    BX,autotmp_0000+-8(SP)MOVL    xs+12(FP),BXMOVL    BX,autotmp_0000+-4(SP)

接下來接收傳入的參數,並將其值儲存在棧上。一個 Go 的 slice 有三個部分:指向其所在的記憶體的指標、長度和容量。指標是 8 位元,長度和容量都是 4 位元。因此這段代碼從 BX 寄存器複製了這些值出來。(參閱這裡瞭解更多關於 slice 的細節)

MOVL    $0,AXMOVL    autotmp_0000+-8(SP),DILEAQ    autotmp_0000+-16(SP),BXMOVQ    (BX),CX

接下來,將 0 放入 AX,用於迴圈變數。將 slice 的長度放入 DI,並且載入指向 xs 元素的指標到 CX。

    JMP     L2L1: INCL    AXL2: CMPL    AX,DI    JGE     L3

現在到達代碼的主體。首先跳轉到 L2 比較 AX 和 DI。如果相等,說明已經計算了 slice 中的所有元素,因此跳到 L3。(也就是 i == len(xs))。

MOVQ    (CX),BPADDQ    $8,CXADDQ    BP,SIJMP     L1

這裡進行了求和。首先從 CX 中擷取值儲存到 BP。然後將 CX 向前移動 8 位元組。最後將 BP 加到 SI 並跳轉到 L1。L1 增加 AX 並且再次開始迴圈。

L3: MOVQ    SI,.noname+16(FP)  RET

結束求和後,將結果儲存在傳遞到函數的所有的參數之後(由於一個 slice 是 16 位元組,所以這裡是 16 位元組)。這時就返回了。

重寫

這裡我重寫了代碼:

// func Sum(xs []int64) int64TEXT ·Sum2(SB),7,$0    MOVQ    $0, SI       // n    MOVQ    xs+0(FP), BX // BX = &xs[0]    MOVL    xs+8(FP), CX // len(xs)    MOVLQSX CX, CX       // len as int64    INCQ    CX           // CX++start:    DECQ    CX           // CX--    JZ done              // jump if CX = 0    ADDQ    (BX), SI     // n += *BX    ADDQ    $8, BX       // BX += 8    JMP startdone:    MOVQ    SI, .noname+16(FP) // return n    RET

希望這會更容易理解一些。

忠告

可以這麼做當然很酷,但是不要忽視了這些忠告:

  • 彙編很難編寫,特別是很難寫好。通常編譯器會比你寫出更快的代碼(從前文來看,Go 編譯器會做得更好)。
  • 彙編僅能運行在一個平台上。在這個例子中,代碼僅能運行在 amd64 上。這個問題有一個解決方案是給 Go 對於 x86 和 arm 不同版本的代碼(像這樣)。
  • 彙編讓你和底層綁定在一起,而標準的 Go 不會。例如,slice 的長度當前是 32 位整數。但是也不是不可能為長整型。當發生這些變化時,這些代碼就被破壞了(也可能是編譯器無法檢測到的更噁心的途徑來破壞)
  • 當前 Go 編譯器不能將彙編編譯為函數的內聯,但是對於小的 Go 函數是可以的。因此使用彙編可能意味著讓你的程式更慢。

對於下面的兩個原因,這還是很有用的:

      有時需要彙編給你帶來一些力量(不論是效能方面的原因,還是一些相當特殊的關於 CPU 的操作)。對於什麼時候應該使用它,Go 源碼包括了若干相當好的例子(可以看看 crypto 和 math)。
      由於它非常容易實踐,所以這絕對是個學習彙編的好途徑。
相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.