5. Go 性能調優之 —— 技巧

原文連接: https://github.com/sxs2473/go...
本文使用 Creative Commons Attribution-ShareAlike 4.0 International 協議進行受權許可。

技巧

本節包含一些優化 Go 代碼的技巧。git

減小分配

確保你的 APIs 不會給調用方增長垃圾。github

考慮這兩個 Read 方法golang

func (r *Reader) Read() ([]byte, error)
func (r *Reader) Read(buf []byte) (int, error)

第一個 Read 方法不帶參數,並將一些數據做爲[]byte返回。 第二個採用[]byte緩衝區並返回讀取的字節數。windows

第一個 Read 方法老是會分配一個緩衝區,這會給 GC 帶來壓力。 第二個填充傳入的緩衝區。緩存

strings vs []bytes

Go 語言中 string 是不可改變的,而 []byte 是可變的。服務器

大多數程序喜歡使用 string,而大多數 IO 操做更喜歡使用 []byte網絡

儘量避免 []bytestring 的轉換,對於一個值來講,最好選定一種表示方式,要麼是[]byte,要麼是string。 一般狀況下,若是你從網絡或磁盤讀取數據,將使用[]byte 表示。閉包

bytes 包也有一些和 strings 包相同的操做函數—— SplitCompareHasPrefixTrim等。併發

實際上, strings 使用和 bytes 包相同的彙編原語。app

使用 []byte 當作 map 的 key

使用 string 做爲 map 的 key 是很常見的,但有時你拿到的是一個 []byte

編譯器爲這種狀況實現特定的優化:

var m map[string]string
v, ok := m[string(bytes)]

如上面這樣寫,編譯器會避免將字節切片轉換爲字符串到 map 中查找,這是很是特定的細節,若是你像下面這樣寫,這個優化就會失效:

key := string(bytes)
val, ok := m[key]

優化字符串鏈接操做

Go 的字符串是不可變的。鏈接兩個字符串就會生成第三個字符串。下面哪一種寫法是最快的呢?

s := request.ID
s += " " + client.Addr().String()
s += " " + time.Now().String()
r = s
var b bytes.Buffer
fmt.Fprintf(&b, "%s %v %v", request.ID, client.Addr(), time.Now())
r = b.String()
r = fmt.Sprintf("%s %v %v", request.ID, client.Addr(), time.Now())
b := make([]byte, 0, 40)
b = append(b, request.ID...)
b = append(b, ' ')
b = append(b, client.Addr().String()...)
b = append(b, ' ')
b = time.Now().AppendFormat(b, "2006-01-02 15:04:05.999999999 -0700 MST")
r = string(b)
% go test -bench=. ./examples/concat/

個人測試結果:

  • go 1.10.3
goos: darwin
goarch: amd64
pkg: test/benchmark
BenchmarkConcatenate-8           2000000               873 ns/op         272 B/op         10 allocs/op
BenchmarkFprintf-8               1000000              1509 ns/op         496 B/op         13 allocs/op
BenchmarkSprintf-8               1000000              1316 ns/op         304 B/op         11 allocs/op
BenchmarkStrconv-8               2000000               620 ns/op         165 B/op          5 allocs/op
PASS
  • go 1.11
goos: darwin
goarch: amd64
pkg: test/benchmark
BenchmarkConcatenate-8        1000000          1027 ns/op         271 B/op          10 allocs/op
BenchmarkFprintf-8            1000000          1707 ns/op         496 B/op          12 allocs/op
BenchmarkSprintf-8            1000000          1412 ns/op         304 B/op          11 allocs/op
BenchmarkStrconv-8            2000000           707 ns/op         165 B/op           5 allocs/op
PASS

全部的基準測試在1.11版本下都變慢了?

已知長度時,切片一次分配好

Append 操做雖然方便,可是有代價。

切片的增加在元素到達 1024 個以前一直是兩倍左右地變化,在到達 1024 個以後以後大約是 25% 地增加。在咱們 append 以後的容量是多少呢?

func main() {
        b := make([]int, 1024)
        fmt.Println("len:", len(b), "cap:", cap(b))
        b = append(b, 99)
        fmt.Println("len:", len(b), "cap:", cap(b))
}
output:
len: 1024 cap: 1024
len: 1025 cap: 1280

若是你使用 append,你可能會複製大量數據併產生大量垃圾。

若是事先知道片的長度,最好預先分配大小以免複製,並確保目標的大小徹底正確。

Before:

var s []string
for _, v := range fn() {
        s = append(s, v)
}
return s

After:

vals := fn()
s := make([]string, len(vals))
for i, v := range vals {
        s[i] = v
}
return s

Goroutines

使 Go 很是適合現代硬件的關鍵特性是 goroutines。goroutine 很容易使用,成本也很低,你能夠認爲它們幾乎是沒有成本的。

Go 運行時是爲運行數以萬計的 goroutines 所設計的,即便有上十萬也在乎料之中。

可是,每一個 goroutine 確實消耗了 goroutine 棧的最小內存量,目前至少爲 2k。

2048 * 1,000,000 goroutines == 2GB 內存,什麼都不幹的狀況下。

這也許算多,也許不算多,同時取決於機器上其餘耗費內存的應用。

要了解 goroutine 何時退出

雖然 goroutine 的啓動和運行成本都很低,但它們的內存佔用是有限的;你不可能建立無限數量的 goroutine。

每次在程序中使用go關鍵字啓動 goroutine 時,你都必須知道這個 goroutine 將如何退出,以及什麼時候退出。

若是你不知道,那這就是潛在的內存泄漏。

在你的設計中,一些 goroutine 可能會一直運行到程序退出。這樣的 goroutine 不該該太多

永遠不要在不知道該何時中止它的狀況下啓動一個 goroutine

實現此目的的一個好方法是利用如 run.Groupworkgroup.Group 這類的東西。

Peter Bourgon has a great presentation on the design behing run.Group from GopherCon EU

進一步閱讀

Go 對一些請求使用高效的網絡輪詢

Go 運行時使用高效的操做系統輪詢機制(kqueue,epoll,windows IOCP等)處理網絡IO。 許多等待的 goroutine 將由一個操做系統線程提供服務。

可是,對於本地文件IO(channel 除外),Go 不實現任何 IO 輪詢。每個*os.File在運行時都消耗一個操做系統線程。

大量使用本地文件IO會致使程序產生數百或數千個線程;這可能會超過操做系統的最大值限制。

您的磁盤子系統可能處理不數百或數千個併發IO請求。

注意程序中的 IO 複雜度

若是你寫的是服務端程序,那麼其主要工做是複用網絡鏈接客戶端和存儲在應用程序中的數據。

大多數服務端程序都是接受請求,進行一些處理,而後返回結果。這聽起來很簡單,但有的時候,這樣作會讓客戶端在服務器上消耗大量(可能無限制)的資源。下面有一些注意事項:

  • 每一個請求的IO操做數量;單個客戶端請求生成多少個IO事件? 若是使用緩存,則它可能平均爲1,或者可能小於1。
  • 服務查詢所需的讀取量;它是固定的?N + 1的?仍是線性的(讀取整個表格以生成結果的最後一頁)?

若是內存都不算快,那麼相對來講,IO操做就太慢了,你應該不惜一切代價避免這樣作。 最重要的是避免在請求的上下文中執行IO——不要讓用戶等待磁盤子系統寫入磁盤,甚至連讀取都不要作。

使用流式 IO 接口

儘量避免將數據讀入[]byte 並傳遞使用它。

根據請求的不一樣,你最終可能會將兆字節(或更多)的數據讀入內存。這會給GC帶來巨大的壓力,而且會增長應用程序的平均延遲。

做爲替代,最好使用io.Readerio.Writer構建數據處理流,以限制每一個請求使用的內存量。

若是你使用了大量的io.Copy,那麼爲了提升效率,請考慮實現io.ReaderFrom / io.WriterTo。 這些接口效率更高,並避免將內存複製到臨時緩衝區。

超時,超時,仍是超時

永遠不要在不知道須要多長時間才能完成的狀況下執行 IO 操做。

你要在使用SetDeadlineSetReadDeadlineSetWriteDeadline進行的每一個網絡請求上設置超時。

您要限制所使用的阻塞IO的數量。 使用 goroutine 池或帶緩衝的 channel 做爲信號量。

var semaphore = make(chan struct{}, 10)

func processRequest(work *Work) {
        semaphore <- struct{}{} // 持有信號量
        // 執行請求
        <-semaphore // 釋放信號量
}

Defer 操做成本如何?

defer 是有成本的,由於它必須爲其執行參數構造一個閉包去執行。

defer mu.Unlock()

至關於

defer func() {
    mu.Unlock()
}()

若是你用它乾的事情不多,defer 的成本就會顯得比較高。一個經典的例子是使用defer對 struct 或 map 進行mutex unlock 操做。 你能夠在這些狀況下避免使用defer

固然,這是爲了提升性能而犧牲可讀性和維護性的狀況。

老是從新考慮這些決定。

避免使用 Finalizers

終結器是一種將行爲附加到即將被垃圾收集的對象的技術。 

所以,終結器是非肯定性的。

要運行 Finalizers,要保證任何東西都不會訪問該對象。 若是你不當心在 map 中保留了對象的引用,則 Finalizers 沒法執行。

Finalizers 做爲 gc 的一部分運行,這意味着它們在運行時是不可預測的,而且它會與 減小 gc 時間 的目標相悖。

當你有一個很是大的堆塊,而且已經優化過你的程序使之減小生成垃圾,Finalizers 可能纔會很快結束。

提示 :參考 SetFinalizer

最小化 cgo

cgo 容許 Go 程序調用 C 語言庫。

C 代碼和 Go 代碼存在於兩個不一樣的世界中,cgo 用來轉換它們。

這種轉換不是沒有代價的,主要取決於它在代碼中的位置,有時成本可能很高。

cgo 調用相似於阻塞IO,它們在操做期間消耗一個系統線程。

不要在一個 tight loop 中調用 C 代碼。

實際上,避免使用 cgo

cgo 的開銷很高。

爲了得到最佳性能,我建議你在應用中避免使用cgo。

  • 若是C代碼須要很長時間,那麼 cgo 自己的開銷就不那麼重要了。
  • 若是你使用 cgo 來調用很是短的C函數,那麼cgo自己的開銷就會顯得很是突出,那麼最好的辦法是在 Go 中重寫該代碼。(由於很短,重寫也沒什麼成本。
  • 若是你就是要使用大量高開銷成本的C代碼在 tight loop 中調用,爲何使用 Go?(直接用 C 寫就行了被。

始終使用最新版發佈的 Go 版本

Go 的舊版本永遠不會變得更好。他們永遠不會獲得錯誤修復或優化。

  • Go 1.4 不該該再使用。
  • Go 1.5 和 1.6 編譯器的速度更慢,但它產生更快的代碼,並具備更快的 GC。
  • Go 1.7 的編譯速度比 1.6 提升了大約 30%,連接速度提升了2倍(優於以前的Go版本)。
  • Go 1.8 在編譯速度方面帶來較小的改進,且在非Intel體系結構的代碼質量方面有顯著的改進。
  • Go 1.9,1.10,1.11 繼續下降 GC 暫停時間並提升生成代碼的質量。

Go 的舊版本不會有任何更新。 不要使用它們。 使用最新版本,你將得到最佳性能。

相關文章
相關標籤/搜索