關於Go1.14,你一定想知道的性能提升與新特性

Go官方團隊將在今年2月份發佈1.14版本。相比較於之前的版本升級,Go1.14在性能提升上做了較大改動,還加入了很多新特性,我們一起來看一下Go1.14都給我們帶來了哪些驚喜吧!

1.性能提升

先列舉幾個Go1.14在性能提升上做的改進。

1.1 defer性能“異常”牛逼

異常牛逼是有多牛逼呢?我們可以通過一個簡單benchmark看一看。用例如下(defer_test.go):

<code>package mainimport ("testing")type channel chan intfunc NoDefer() {ch1 := make(channel, 10)close(ch1)}func Defer() {ch2 := make(channel, 10)defer close(ch2)}func BenchmarkNoDefer(b *testing.B) {for i := 0; i < b.N; i++ {NoDefer()}}func BenchmarkDefer(b *testing.B) {for i := 0; i < b.N; i++ {Defer()}}/<code>

我們分別使用Go1.13版本和Go1.14版本進行測試,關於Go多個版本的管理切換,推薦大家使用gvm,非常的方便。首先使用Go1.13版本,只需要命令:gvm use go1.13;之後運行命令:go test -bench=. -v,結果如下:

<code>goos: darwingoarch: amd64pkg: github.com/GuoZhaoran/myWebSites/data/goProject/deferBenchmarkNoDefer-4   15759076        74.5 ns/opBenchmarkDefer-4     11046517       102 ns/opPASSok  github.com/GuoZhaoran/myWebSites/data/goProject/defer3.526s/<code>

可以看到,Go1.13版本調用defer關閉channel的性能開銷還是蠻大的,op幾乎差了30ns。切換到go1.14:gvm use go1.14;再次運行命令:go test -bench=. -v,下面的結果一定會亮瞎了小夥伴的雙眼:

<code>goos: darwingoarch: amd64pkg: github.com/GuoZhaoran/myWebSites/data/goProject/deferBenchmarkNoDeferBenchmarkNoDefer-4   13094874        80.3 ns/opBenchmarkDeferBenchmarkDefer-4     13227424        80.4 ns/opPASSok  github.com/GuoZhaoran/myWebSites/data/goProject/defer2.328s/<code>

Go1.14版本使用defer關閉channel幾乎0開銷!

關於這一改進,官方給出的回應是:Go1.14提高了defer的大多數用法的性能,幾乎0開銷!defer已經可以用於對性能要求很高的場景了。

關於defer,在Go1.13版本已經做了一些的優化,相較於Go1.12,defer大多數用法性能提升了30%。而Go1.14的此次改進更是激動人心!關於Go1.14對defer優化的原理和細節,筆者還沒有收集到參考資料,相信很快就會有大神整理出來,大家可以關注一下。關於Go語言defer的設計原理、Go1.13對defer做了哪些改進,推薦給大家下面幾篇文章:

  • 深入理解defer上[1]
  • 深入理解defer實現機制下[2]
  • Go1.13 defer性能是如何提高的

1.2 goroutine支持異步搶佔

Go語言調度器的性能隨著版本迭代表現的越來越優異,我們來了解一下調度器使用的G-M-P模型。先是一些概念:

  • G(Goroutine):goroutine,由關鍵字go創建
  • M(Machine):在Go中稱為Machine,可以理解為工作線程
  • P(Processor) :處理器 P 是線程 M 和 Goroutine 之間的中間層(並不是CPU)

M必須持有P才能執行G中的代碼,P有自己本地的一個運行隊列runq,由可運行的G組成,下圖展示了 線程 M、處理器 P 和 goroutine 的關係。

關於Go1.14,你一定想知道的性能提升與新特性

Go語言調度器的工作原理就是處理器P從本地隊列中依次選擇goroutine 放到線程 M 上調度執行,每個P維護的G可能是不均衡的,為此調度器維護了一個全局G隊列,當P執行完本地的G任務後,會嘗試從全局隊列中獲取G任務運行(需要加鎖),當P本地隊列和全局隊列都沒有可運行的任務時,會嘗試偷取其他P中的G到本地隊列運行(任務竊取)。

在Go1.1版本中,調度器還不支持搶佔式調度,只能依靠 goroutine 主動讓出 CPU 資源,存在非常嚴重的調度問題:

  • 單獨的 goroutine 可以一直佔用線程運行,不會切換到其他的 goroutine,造成飢餓問題
  • 垃圾回收需要暫停整個程序(Stop-the-world,STW),如果沒有搶佔可能需要等待幾分鐘的時間,導致整個程序無法工作

Go1.12中編譯器在特定時機插入函數,通過函數調用作為入口觸發搶佔,實現了協作式的搶佔式調度。但是這種需要函數調用主動配合的調度方式存在一些邊緣情況,就比如說下面的例子:

<code>package mainimport ("runtime""time")func main() {runtime.GOMAXPROCS(1)go func() {for {}}()time.Sleep(time.Millisecond)println("OK")}/<code>

其中創建一個goroutine並掛起, main goroutine 優先調用了 休眠,此時唯一的 P 會轉去執行 for 循環所創建的 goroutine,進而 main goroutine 永遠不會再被調度。換一句話說在Go1.14之前,上邊的代碼永遠不會輸出OK,因為這種協作式的搶佔式調度是不會使一個沒有主動放棄執行權、且不參與任何函數調用的goroutine被搶佔。

Go1.14 實現了基於信號的真搶佔式調度解決了上述問題。Go1.14程序啟動時,在 runtime.sighandler 函數中註冊了 SIGURG 信號的處理函數 runtime.doSigPreempt,在觸發垃圾回收的棧掃描時,調用函數掛起goroutine,並向M發送信號,M收到信號後,會讓當前goroutine陷入休眠繼續執行其他的goroutine。

Go語言調度器的實現機制是一個非常深入的話題。下邊推薦給讀者幾篇文章,特別值得探索學習:

  • 調度系統設計精要
  • The Go Scheduler[3]
  • 歐長坤大神的作品《Go語言原本》[4]

1.3 time.Timer定時器性能得到“巨幅”提升

我們先來看一下官方的benchmark數據吧。數據來源[5]

<code>Changes in the time package benchmarks:name                      old time/op  new time/op  deltaAfterFunc-12              1.57ms ± 1%  0.07ms ± 1%  -95.42%  (p=0.000 n=10+8)After-12                  1.63ms ± 3%  0.11ms ± 1%  -93.54%  (p=0.000 n=9+10)Stop-12                   78.3µs ± 3%  73.6µs ± 3%   -6.01%  (p=0.000 n=9+10)SimultaneousAfterFunc-12   138µs ± 1%   111µs ± 1%  -19.57%  (p=0.000 n=10+9)StartStop-12              28.7µs ± 1%  31.5µs ± 5%   +9.64%  (p=0.000 n=10+7)Reset-12                  6.78µs ± 1%  4.24µs ± 7%  -37.45%  (p=0.000 n=9+10)Sleep-12                   183µs ± 1%   125µs ± 1%  -31.67%  (p=0.000 n=10+9)Ticker-12                 5.40ms ± 2%  0.03ms ± 1%  -99.43%  (p=0.000 n=10+10)Sub-12                     114ns ± 1%   113ns ± 3%     ~     (p=0.069 n=9+10)Now-12                    37.2ns ± 1%  36.8ns ± 3%     ~     (p=0.287 n=8+8)NowUnixNano-12            38.1ns ± 2%  37.4ns ± 3%   -1.87%  (p=0.020 n=10+9)Format-12                  252ns ± 2%   195ns ± 3%  -22.61%  (p=0.000 n=9+10)FormatNow-12               234ns ± 1%   177ns ± 2%  -24.34%  (p=0.000 n=10+10)MarshalJSON-12             320ns ± 2%   250ns ± 0%  -21.94%  (p=0.000 n=8+8)MarshalText-12             320ns ± 2%   245ns ± 2%  -23.30%  (p=0.000 n=9+10)Parse-12                   206ns ± 2%   208ns ± 4%     ~     (p=0.084 n=10+10)ParseDuration-12          89.1ns ± 1%  86.6ns ± 3%   -2.78%  (p=0.000 n=10+10)Hour-12                   4.43ns ± 2%  4.46ns ± 1%     ~     (p=0.324 n=10+8)Second-12                 4.47ns ± 1%  4.40ns ± 3%     ~     (p=0.145 n=9+10)Year-12                   14.6ns ± 1%  14.7ns ± 2%     ~     (p=0.112 n=9+9)Day-12                    20.1ns ± 3%  20.2ns ± 1%     ~     (p=0.404 n=10+9)/<code>

從基準測試的結果可以看出Go1.14 time包中AfterFunc、After、Ticker的性能都得到了“巨幅”提升。

在Go1.10之前的版本中,Go語言使用1個全局的四叉小頂堆維護所有的timer。實現機制是這樣的:

關於Go1.14,你一定想知道的性能提升與新特性

img

看圖有些抽象,下面用文字描述一下上述過程:

  • G6 調用函數創建了一個timer,系統會產生一個TimerProc,放到本地隊列的頭部,TimerProc也是一個G,由系統調用
  • P調度執行TimerProc的G時,調用函數讓出P,G是在M1上執行的,線程休眠,G6阻塞在channel上,保存到堆上
  • 喚醒P,獲得M3繼續調度執行任務G1、G4,執行完所有任務之後讓出P,M3休眠
  • TimerProc休眠到期後,重新喚醒P,執行TimerProc將G6恢復到P的本地隊列,等待執行。TimerProc則再次和M1休眠,等待下一次創建timer時被喚醒
  • P再次被喚醒,獲得M3,執行任務G6

對Timer的工作原理可能描述的比較粗略,但我們可以看出執行一次Timer任務經歷了好多次M/P切換,這種系統開銷是非常大的,而且從全局唯一堆上遍歷timer恢復G到P是需要加鎖的,導致Go1.10之前的計時器性能比較差,但是在對於計時要求不是特別苛刻的場景,也是完全可以勝任的。

Go1.10將timer堆增加到了64個,使用協程所屬的ProcessID % 64來計算定時器存入的相應的堆,也就是說當P的數量小於64時,每個P只會把timer存到1個堆,這樣就避免了加鎖帶來的性能損耗,只有當P設置大於64時才會出現多個P分佈於同一個堆中,這個時候還是需要加鎖,雖然很少有服務將P設置的大於64。

關於Go1.14,你一定想知道的性能提升與新特性

Go1.10對計時器的優化

但是正如我們前邊的分析,提升Go計時器性能的關鍵是消除喚醒一個 timer 時進行 M/P 頻繁切換的開銷,Go1.10並沒有解決根本問題。Go1.14做到了!直接在每個P上維護自己的timer堆,像維護自己的一個本地隊列runq一樣。

關於Go1.14,你一定想知道的性能提升與新特性

Go1.14對計時器的優化

不得不說這種設計實在是太棒了,首先解決了最關鍵的問題,喚醒timer不用進行頻繁的M/P切換,其次不用再維護TimerProc這個系統協程了(Go1.14刪除了TimerProc代碼的實現),同時也不用考慮因為競爭使用鎖了。timer的調度時機更多了,在P對G調度的時候,都可以檢查一次timer是否到期,而且像G任務一樣,當P本地沒有timer時,可以嘗試從其他的P偷取一些timer任務運行。

關於Go1.14 time.Timer的實現,推薦給大家B站上的視頻,我從中受益很多:Go time.Timer源碼分析[6]

2. 語言層面的變化

2.1 允許嵌入具有重疊方法集的接口

這應該是Go1.14在語言層面上最大的改動了,如下的接口定義在Go1.14之前是不允許的:

<code>type ReadWriteCloser interface {io.ReadCloserio.WriteCloser}/<code>

因為io.ReadCloser和io.WriteCloser中Close方法重複了,編譯時會提示:duplicate method Close。Go1.14開始允許相同簽名的方法可以內嵌入一個接口中,注意是相同簽名,下邊的代碼在Go1.14依然不能夠執行,因為MyCloser接口中定義的Close方法和io.ReadCloser接口定義的Close方法的簽名不同。

<code>type MyCloser interface {Close()}type ReadWriteCloser interface {io.ReadCloserMyCloser}/<code>

將MyCloser的Close方法簽名修改為:

<code>type MyCloser interface {Close() error}/<code>

這樣代碼就可以在Go1.14版本中build了!輕鬆實現接口定義的重載。

2.2 testing包的T、B和TB都加上了CleanUp方法

在並行測試和子測試中,CleanUp(f func())非常有用,它將以後進先出的方式執行f(如果註冊多個的話)。

舉一個例子:

<code>func TestSomeing(t *testing.T) {t.CleanUp(func() {fmt.Println("Cleaning Up!")})t.Run(t.Name(), func(t *testing.T) {})}/<code>

可以在test或者benchmark結束後調用t.CleanUp 或 b.CleanUp做一些收尾統計工作,非常有用!

2.3 添加了新包hash/maphash

這個新包提供了字節序列上的hash函數。這些哈希函數用於實現哈希表或其他的數據結構,這些哈希表或其他數據結構需要將任意字符串或字節序列映射為整數的均勻分佈。這些hash函數具有抗衝突性,但不是加密安全的。

2.4 WebAssembly的變化

對WebAssembly感興趣的小夥伴注意了,Go1.14對WebAssembly做了如下改動:

  • 可以通過js.Value對象從Go引用的Javascript值進行垃圾回收
  • js.Value 值不再使用 == 操作符來比較,必須使用Equal函數
  • js.Value 增加了IsUndefined,IsNull,IsNaN函數

2.5 reflect包的變化

reflect在StructField元素中設置了PkgPath字段,StructOf支持使用未導出字段創建結構類型。

2.6 語言層面其他改動

Go1.14在語言層面還做了很多其他的改動,下面列舉一些(不是很全面):

代碼包改動crypto/tls移除了對SSLv3的支持,默認開啟TLS1.3,通過Config.MaxVersion字段配置其版本而不是通過DEBUG環境變量進行配置strconvNumError類型新增加了一個UnWrap方法,可以用於找到轉換失敗的原因,可以用Errors.Is來查看NumError值是否是底層錯誤:strconv.ErrRange 或 strconv.ErrSyntaxruntimeruntime.Goexit不再被遞歸的panic/recover終止runtime/pprof生成的profile不再包括用於內聯標記的偽PC。內聯函數的符號信息以pprof工具期望的格式編碼net/http新的Header方法的Values可用於獲取規範化Key關聯的所有制,新的Transport字段DialTLSContext可用於指定可選的以非代理https請求創建TLS連接的dail功能net/http/httptestServer的字段EnableHTTP2可用於在test server上支持HTTP/2mime.js和.mjs文件的默認類型是text/javascript,而不是application/javascirptmime/multipart新的Reader方法NextRawPart支持獲取下一個MIME的部分,而不需要透明的解碼引用的可打印數據signal在Windows上,CTRL_CLOSE_EVENT、CTRL_LOGOFF_EVENT、CTRL_SHUTDOWN_EVENT將生成一個syscall.SIGTERM信號,類似於Control-C和Control-Break如何生成syscall.SIGINT信號math新的FMA函數在浮點計算xy + z的時候,不對 xy計算進行舍入處理(幾種體系結構使用專用的硬件指令來實現此計算,以提高性能)math/bits新的函數Rem,Rem32,Rem64即使在商溢出時也支持計算餘數go/buildContext類型有了一個新字段Dir,用於設置build的工作目錄unicode整個系統中的unicode包和相關支持已經從Unicode1.0升級到了Unicode12.0,增加了554個新字符,其中包括4個腳本和61個新emoji

3. 工具的變化

關於Go1.14中對工具的完善,主要說一下go mod和go test,Go官方肯定希望開發者使用官方的包管理工具,Go1.14完善了很多功能,如果大家在業務開發中對go mod有其他的功能需求,可以給官方提issue。

go mod 主要做了以下改進:

  • incompatiable versions:如果模塊的最新版本包含go.mod文件,則除非明確要求或已經要求該版本,否則go get將不再升級到該模塊的不兼容主要版本。直接從版本控制中獲取時,go list還會忽略此模塊的不兼容版本,但如果由代理報告,則可能包括這些版本。
  • go.mod文件維護:除了go mod tidy之外的go命令不再刪除require指令,該指令指定了間接依賴版本,該版本已由主模塊的其他依賴項隱含。除了go mod tidy之外的go命令不再編輯go.mod文件,如果更改只是修飾性的。
  • Module下載:在module模式下,go命令支持SVN倉庫,go命令現在包括來自模塊代理和其他HTTP服務器的純文本錯誤消息的摘要。如果錯誤消息是有效的UTF-8,且包含圖形字符和空格,只會顯示錯誤消息。

go test的改動比較小:

  • go test -v現在將t.Log輸出流式傳輸,而不是在所有測試數據結束時輸出。

4. 生態建設

關於go語言的生態建設主要說一下go.dev,2019年11月14日Go 官方團隊在 golang-nuts 郵件組宣佈 go.dev 上線。我們初次使用go.dev,發現它提供了 godoc.org 的文檔,界面更加友好。godoc.org 也給出聲明將重定向到go.dev,可以看出,Go官方團隊會將go.dev作為生態建設的重點。

關於Go1.14,你一定想知道的性能提升與新特性

img

pkg.go.dev 是 go.org的配套網站,裡邊有精選用例和其他資源的信息,提供了godoc.org 之類的 Go 文檔,但它更懂模塊,並提供了有關軟件包先前版本的信息,它還可以檢測並顯示許可證,並具有更好的搜索算法。

推薦大家使用!

5. 未來展望

我們先來說說泛性吧!Go語言因為一直缺少泛型被很多開發者詬病。語言的設計者需要在編程效率、編譯速度和運行速度三者進行權衡和選擇,泛型的引入一定會影響編譯速度和運行速度,同時也會增加編譯器的複雜度,所以社區在考慮泛型時也非常謹慎。Go 語言團隊認為加入泛型並不緊急,更重要的是完善運行時機制,包括 調度器、垃圾收集器等功能。但是開發者的呼聲日益強烈,Go官方也承諾會在2.0加入泛型。小道消息,2020年末,Go語言可能會推出泛型,大家期待一下!關於Go語言為什麼沒有泛型,推薦大家一篇文章:為什麼 Go 語言沒有泛型 · Why's THE Design?[7]

再來說說Go語言的錯誤處理吧。try proposal獲得了很多人的支持,但是也有很多人反對,大家可以關注一下issue #32825[8]。結論是:Go已經放棄了這一提案!這些思想還沒有得到充分的發展,尤其考慮到更改語言的實現成本時,所以有關枚舉和不可變類型,Go語言團隊最近也是不給予考慮實現的。

Go1.14也有一些計劃中但是未完成的工作,Go1.14嘗試優化頁分配器(page allocator),能夠實現在GOMAXPROCS值比較大時,顯著減少鎖競爭。這一改動影響很大,能顯著的提高Go並行能力,也會進一步提升timer的性能。但是由於實現起來比較複雜,有一些來不及解決的問題,要delay到Go1.15完成了。

展望Go語言的未來發展,官方肯定會努力將調度器、運行時和垃圾回收做的更好,Go語言的性能也會越來越出眾。對於工具鏈會不斷豐富調整相應功能,為開發者提供方便。同時,Go也會不斷完善其生態,工具包、社區成熟的應用越來越多。讓我們一起期待吧!


鏈接:https://juejin.im/post/5e3f9990e51d4526cc3b1672

[1]

深入理解defer上: https://zhuanlan.zhihu.com/p/68702577

[2]

深入理解defer實現機制下: https://zhuanlan.zhihu.com/p/69455275

[3]

The Go Scheduler: http://morsmachine.dk/go-scheduler

[4]

歐長坤大神的作品《Go語言原本》: https://changkun.de/golang/zh-cn/part2runtime/ch06sched/

[5]

數據來源: https://github.com/golang/go/commit/6becb033341602f2df9d7c55cc23e64b925bbee2

[6]

Go time.Timer源碼分析: https://www.bilibili.com/video/av81849820?from=search&seid=12782037950659289264

[7]

為什麼 Go 語言沒有泛型 · Why's THE Design?: https://draveness.me/whys-the-design-go-generics

[8]

#32825: https://github.com/golang/go/issues/32825


分享到:


相關文章: