在 Golang 中針對 int64 類型最佳化 abs()

來源:互聯網
上載者:User
這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。Go 語言沒有內建 `abs()` 標準函數來計算整數的絕對值,這裡的絕對值是指負數、正數的非負表示。我最近為瞭解決 [Advent of Code 2017](http://adventofcode.com/2017/about) 上邊的 [Day 20](http://adventofcode.com/2017/day/20) 難題,自己實現了一個 `abs()` 函數。如果你想學點新東西或試試身手,可以去一探究竟。Go 實際上已經在 `math` 包中實現了 `abs()` : [math.Abs](https://golang.org/pkg/math/#Abs) ,但對我的問題並不適用,因為它的輸入輸出的實值型別都是 `float64`,我需要的是 `int64`。通過參數轉換是可以使用的,不過將 `float64` 轉為 `int64` 會產生一些開銷,且轉換值很大的數會發生截斷,這兩點都會在文章說清楚。文章 [Pure Go math.Abs outperforms assembly version](https://groups.google.com/forum/#!topic/golang-dev/nP5mWvwAXZo) 討論了針對浮點數如何最佳化 `math.Abs`,不過這些最佳化的方法因底層編碼不同,不能直接應用在整型上。文章中的源碼和測試案例在 [cavaliercoder/go-abs](https://github.com/cavaliercoder/go-abs)## 類型轉換 VS 分支控制的方法對我來說取絕對值最簡單的函數實現是:輸入參數 n 大於等於 0 直接返回 n,小於零則返回 -n(負數取反為正),這個取絕對值的函數依賴分支控制結構來計算絕對值,就命名為:`abs.WithBranch````gopackage absfunc WithBranch(n int64) int64 {if n < 0 {return -n}return n}```成功返回 n 的絕對值,這就是 [Go v1.9.x](https://github.com/golang/go/blob/release-branch.go1.9/src/math/abs.go) `math.Abs` 對 float64 取絕對值的實現。不過當進行類型轉換(int64 to float64)再取絕對值時,1.9.x 是否做了改進?我們可以驗證一下:```gopackage absfunc WithStdLib(n int64) int64 {return int64(math.Abs(float64(n)))}```上邊的代碼中,將 n 先從 `int64` 轉成 `float64`,通過 `math.Abs` 取到絕對值後再轉回 `int64`,多次轉換顯然會造成效能開銷。可以寫一個基準測試來驗證一下:```console$ go test -bench=.goos: darwingoarch: amd64pkg: github.com/cavaliercoder/absBenchmarkWithBranch-8 2000000000 0.30 ns/opBenchmarkWithStdLib-8 2000000000 0.79 ns/opPASSok github.com/cavaliercoder/abs 2.320s```測試結果:0.3 ns/op, `WithBranch` 要快兩倍多,它還有一個優勢:在將 int64 的大數轉化為 IEEE-754 標準的 float64 不會發生截斷(丟失超出精度的值)舉個例子:`abs.WithBranch(-9223372036854775807)` 會正確返回 9223372036854775807。但 `WithStdLib(-9223372036854775807)` 則在類型轉換區間發生了溢出,返回 -9223372036854775808,在大的正數輸入時, `WithStdLib(9223372036854775807)` 也會返回不正確的負數結果。不依賴分支控制的方法取絕對值的方法對有符號整數顯然更快更准,不過還有更好的辦法嗎?我們都知道不依賴分支控制的方法的代碼破壞了程式的運行順序,即 [pipelining processors](http://euler.mat.uson.mx/~havillam/ca/CS323/0708.cs-323007.html) 無法預知程式的下一步動作。## 與不依賴分支控制的方法不同的方案[Hacker’s Delight](https://books.google.com.au/books?id=VicPJYM0I5QC&lpg=PA18&ots=2o-SROAuXq&dq=hackers%20delight%20absolute&pg=PA18#v=onepage&q=hackers%20delight%20absolute&f=false) 第二章介紹了一種無分支控制的方法,通過 [Two’s Complement](https://www.cs.cornell.edu/~tomf/notes/cps104/twoscomp.html) 計算有符號整數的絕對值。為計算 x 的絕對值,先計算 `x >> 63` ,即 x 右移 63 位(擷取最高位符號位),如果你對熟悉不帶正負號的整數的話, 應該知道如果 x 是負數則 y 是 1,否者 y 為 0接著再計算 `(x ⨁ y) - y` :x 與 y 異或後減 y,即是 x 的絕對值。可以直接使用高效的彙編實現,代碼如下:```gofunc WithASM(n int64) int64``````asm// abs_amd64.sTEXT ·WithASM(SB),$0MOVQ n+0(FP), AX // copy input to AXMOVQ AX, CX // y ← xSARQ $63, CX // y ← y >> 63XORQ CX, AX // x ← x ⨁ ySUBQ CX, AX // x ← x - yMOVQ AX, ret+8(FP) // copy result to return valueRET```我們先命名這個函數為 `WithASM`,分離命名與實現,函數體使用 [Go 的彙編](https://golang.org/doc/asm) 實現,上邊的代碼只適用於 AMD64 架構的系統,我建議你的檔案名稱加上 `_amd64.s` 的尾碼。`WithASM` 的基準測試結果:```shell$ go test -bench=.goos: darwingoarch: amd64pkg: github.com/cavaliercoder/absBenchmarkWithBranch-8 2000000000 0.29 ns/opBenchmarkWithStdLib-8 2000000000 0.78 ns/opBenchmarkWithASM-8 2000000000 1.78 ns/opPASSok github.com/cavaliercoder/abs 6.059s```這就比較尷尬了,這個簡單的基準測試顯示無分支控制結構高度簡潔的代碼跑起來居然很慢:1.78 ns/op,怎麼會這樣呢?## 編譯選項我們需要知道 Go 的編譯器是怎麼最佳化執行 `WithASM` 函數的,編譯器接受 `-m` 參數來列印出最佳化的內容,在 `go build` 或 `go test` 中加上 `-gcflags=-m` 使用:運行效果:```console$ go tool compile -m abs.go# github.com/cavaliercoder/abs./abs.go:11:6: can inline WithBranch./abs.go:21:6: can inline WithStdLib./abs.go:22:23: inlining call to math.Abs```對於我們這個簡單的函數,Go 的編譯器支援 [function inlining](https://github.com/golang/go/wiki/CompilerOptimizations#function-inlining),函數內聯是指在調用我們函數的地方直接使用這個函數的函數體來代替。舉個例子:```gopackage mainimport ("fmt""github.com/cavaliercoder/abs")func main() {n := abs.WithBranch(-1)fmt.Println(n)}``` 實際上會被編譯成:```gopackage mainimport "fmt"func main() {n := -1if n < 0 {n = -n}fmt.Println(n)}```根據編譯器的輸出,可以看出 `WithBranch` 和 `WithStdLib` 在編譯時間候被內聯了,但是 `WithASM` 沒有。對於 `WithStdLib`,即使底層調用了 `math.Abs` 但編譯時間依舊被內聯。因為 `WithASM` 函數沒法內聯,每個調用它的函數會在調用上產生額外的開銷:為 `WithASM` 重新分配棧記憶體、複製參數及指標等等。如果我們在其他函數中不使用內聯會怎麼樣?可以寫個簡單的樣本程式:```gopackage abs//go:noinlinefunc WithBranch(n int64) int64 {if n < 0 {return -n}return n}```重新編譯,我們會看到編譯器最佳化內容變少了:```go$ go tool compile -m abs.goabs.go:22:23: inlining call to math.Abs```基準測試的結果:```console$ go test -bench=.goos: darwingoarch: amd64pkg: github.com/cavaliercoder/absBenchmarkWithBranch-8 1000000000 1.87 ns/opBenchmarkWithStdLib-8 1000000000 1.94 ns/opBenchmarkWithASM-8 2000000000 1.84 ns/opPASSok github.com/cavaliercoder/abs 8.122s```可以看出,現在三個函數的平均執行時間幾乎都在 1.9 ns/op 左右。你可能會覺得每個函數的調用開銷在 1.5ns 左右,這個開銷的出現否定了我們 `WithBranch` 函數中的速度優勢。我從上邊學到的東西是, `WithASM` 的效能要優於編譯器實作類別型安全、記憶體回收和函數內聯帶來的效能,雖然大多數情況下這個結論可能是錯誤的。當然,這其中是有特例的,比如提升 [SIMD](https://goroutines.com/asm) 的加密效能、流媒體編碼等。## 只使用一個內嵌函式Go 編譯器無法內聯由彙編實現的函數,但是內聯我們重寫後的普通函數是很容易的:```gopackage absfunc WithTwosComplement(n int64) int64 {y := n >> 63 // y ← x >> 63return (n ^ y) - y // (x ⨁ y) - y}```編譯結果說明我們的方法被內聯了:```shell$ go tool compile -m abs.go...abs.go:26:6: can inline WithTwosComplement```但是效能怎麼樣呢?結果表明:當我們啟用函數內聯時,效能與 `WithBranch` 很相近了:```shell$ go test -bench=.goos: darwingoarch: amd64pkg: github.com/cavaliercoder/absBenchmarkWithBranch-8 2000000000 0.29 ns/opBenchmarkWithStdLib-8 2000000000 0.79 ns/opBenchmarkWithTwosComplement-8 2000000000 0.29 ns/opBenchmarkWithASM-8 2000000000 1.83 ns/opPASSok github.com/cavaliercoder/abs 6.777s```現在函數調用的開銷消失了,`WithTwosComplement` 的實現要比 `WithASM` 的實現好得多。來看看編譯器在編譯 `WithASM` 時做了些什嗎?使用 `-S` 參數告訴編譯器列印出彙編過程:```shell$ go tool compile -S abs.go..."".WithTwosComplement STEXT nosplit size=24 args=0x10 locals=0x00x0000 00000 (abs.go:26) TEXT "".WithTwosComplement(SB), NOSPLIT, $0-160x0000 00000 (abs.go:26) FUNCDATA $0, gclocals·f207267fbf96a0178e8758c6e3e0ce28(SB)0x0000 00000 (abs.go:26) FUNCDATA $1, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)0x0000 00000 (abs.go:26) MOVQ "".n+8(SP), AX0x0005 00005 (abs.go:26) MOVQ AX, CX0x0008 00008 (abs.go:27) SARQ $63, AX0x000c 00012 (abs.go:28) XORQ AX, CX0x000f 00015 (abs.go:28) SUBQ AX, CX0x0012 00018 (abs.go:28) MOVQ CX, "".~r1+16(SP)0x0017 00023 (abs.go:28) RET...```編譯器在編譯 `WithASM` 和 `WithTwosComplement` 時,做的事情太像了,編譯器在這時才有正確配置和跨平台的優勢,可加上 `GOARCH=386` 選項再次編譯產生相容 32 位系統的程式。最後關於記憶體配置,上邊所有函數的實現都是比較理想的情況,我運行 `go test -bench=. -benchme`,觀察對每個函數的輸出,顯示並沒有發生記憶體配置。### 總結`WithTwosComplement` 的實現方式在 Go 中提供了較好的可移植性,同時實現了函數內聯、無分支控制的代碼、零記憶體配置與避免類型轉換導致的值截斷。基準測試沒有顯示出無分支控制比有分支控制的優勢,但在理論上,無分支控制的代碼在多種情況下效能會更好。最後,我對 int64 的 abs 實現如下:```gofunc abs(n int64) int64 {y := n >> 63return (n ^ y) - y}```

via: http://cavaliercoder.com/blog/optimized-abs-for-int64-in-go.html

作者:Ryan Armstrong 譯者:wuYinBest 校對:rxcai

本文由 GCTT 原創編譯,Go語言中文網 榮譽推出

本文由 GCTT 原創翻譯,Go語言中文網 首發。也想加入譯者行列,為開源做一些自己的貢獻嗎?歡迎加入 GCTT!
翻譯工作和譯文發表僅用於學習和交流目的,翻譯工作遵照 CC-BY-NC-SA 協議規定,如果我們的工作有侵犯到您的權益,請及時聯絡我們。
歡迎遵照 CC-BY-NC-SA 協議規定 轉載,敬請在本文中標註並保留原文/譯文連結和作者/譯者等資訊。
文章僅代表作者的知識和看法,如有不同觀點,請樓下排隊吐槽

487 次點擊  
相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在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.