到現(xiàn)在為止卿拴,我已經(jīng)忘記了我在寫什么,但我確定這篇文章是關(guān)于Go語言的柬甥。這主要是一篇趁桃,關(guān)于運(yùn)行速度踊沸,而不是開發(fā)速度的文章——這兩種速度是有區(qū)別的。
我曾經(jīng)和很多聰明的人一起工作权纤。我們很多人都對(duì)性能問題很癡迷钓简,我們之前所做的是嘗試逼近能夠預(yù)期的(性能)的極限。應(yīng)用引擎有一些非常嚴(yán)格的性能要求汹想,所以我們才會(huì)做出改變外邓。自從使用了Go語言之后,我們已經(jīng)學(xué)習(xí)到了很多提升性能以及讓Go在系統(tǒng)編程中正常運(yùn)轉(zhuǎn)的方法欧宜。
Go的簡(jiǎn)單和原生并發(fā)使其成為一門非常有吸引力的后端開發(fā)語言坐榆,但更大的問題是它如何應(yīng)對(duì)延遲敏感的應(yīng)用場(chǎng)景?是否值得犧牲語言的簡(jiǎn)潔性使其速度更快冗茸?讓我們來一起看一下Go語言性能優(yōu)化的幾個(gè)方面:語言特性席镀、內(nèi)存管理、并發(fā)夏漱,并根據(jù)這些做出合適的優(yōu)化決策豪诲。所有這里介紹的測(cè)試代碼都在這里.
Channels
Channel在Go語言中受到了很多的關(guān)注,因?yàn)樗且粋€(gè)方便的并發(fā)工具挂绰,但是了解它對(duì)性能的影響也很重要屎篱。在大多數(shù)場(chǎng)景下它的性能已經(jīng)“足夠好”了,但是在某些延時(shí)敏感的場(chǎng)景中葵蒂,它可能會(huì)成為瓶頸交播。Channel并不是什么黑魔法。在Channel的底層實(shí)現(xiàn)中践付,使用的還是鎖秦士。在沒有鎖競(jìng)爭(zhēng)的單線程應(yīng)用中,它能工作的很好永高,但是在多線程場(chǎng)景下隧土,性能會(huì)急劇下降提针。我們可以很容易的使用無鎖隊(duì)列ring buffer來替代channel的功能。
第一個(gè)性能測(cè)試對(duì)比了單線程buffer channel和ring buffer(一個(gè)生產(chǎn)者和一個(gè)消費(fèi)者)曹傀。先看看單核心的情況(GOMAXPROCS = 1)
BenchmarkChannel 3000000 512 ns/op
BenchmarkRingBuffer 20000000 80.9 ns/op
正如你所看到的辐脖,ring buffer大約能快6倍(如果你不熟悉Go的性能測(cè)試工具,中間的數(shù)字表示執(zhí)行次數(shù)皆愉,最后一個(gè)數(shù)組表示每次執(zhí)行花費(fèi)的時(shí)間)嗜价。接下來,我們?cè)倏聪抡{(diào)整GOMAXPROCS = 8的情況亥啦。
BenchmarkChannel-8 3000000 542 ns/op
BenchmarkRingBuffer-8 10000000 182 ns/op
ring buffer快了近三倍
Channel通常用于給worker分配任務(wù)炭剪。在下面的測(cè)試中,我們對(duì)比一下多個(gè)reader讀取同一個(gè)channel或者ring buffer的情況翔脱。設(shè)置GOMAXPROCS = 1 測(cè)試結(jié)果表明channel在單核程應(yīng)用中性能表現(xiàn)尤其的好奴拦。
BenchmarkChannelReadContention 10000000 148 ns/op
BenchmarkRingBufferReadContention 10000 390195 ns/op
然而,ring buffer在多核心的情況下速度更快些:
BenchmarkChannelReadContention-8 1000000 3105 ns/op
BenchmarkRingBufferReadContention-8 3000000 411 ns/op
最后届吁,我們來看看多個(gè)reader和多個(gè)writer的場(chǎng)景错妖。從下面的對(duì)比同樣能夠看到ring buffer在多核心時(shí)更好些。
BenchmarkChannelContention 10000 160892 ns/op
BenchmarkRingBufferContention 28068 34344 ns/op
BenchmarkChannelContention-8 5000 314428 ns/op
BenchmarkRingBufferContention-8 10000 182557 ns/op
ring buffer只使用CAS操作達(dá)到線程安全疚沐。我們可以看到暂氯,在決定選擇channel還是ring buffer時(shí)很大程度上取決于系統(tǒng)的核數(shù)。對(duì)于大多數(shù)系統(tǒng)亮蛔, GOMAXPROCS> 1痴施,所以無鎖的ring buffer往往是一個(gè)更好的選擇。Channel在多核心系統(tǒng)中則是一個(gè)比較糟糕的選擇究流。
defer
defer是提高可讀性和避免資源未釋放的非常有用的關(guān)鍵字辣吃。例如,當(dāng)我們打開一個(gè)文件進(jìn)行讀取時(shí)芬探,我們需要在結(jié)束讀取時(shí)關(guān)閉它神得。如果沒有defer關(guān)鍵字,我們必須確保在函數(shù)的每個(gè)返回點(diǎn)之前關(guān)閉文件偷仿。
func findHelloWorld(filename string) error {
file, err := os.Open(filename)
if err != nil {
return err
}
scanner := bufio.NewScanner(file)
for scanner.Scan() {
if scanner.Text() == "hello, world!" {
file.Close()
return nil
}
}
file.Close()
if err := scanner.Err(); err != nil {
return err
}
return errors.New("Didn't find hello world")
}
這樣很容易出錯(cuò)哩簿,因?yàn)楹苋菀自谌魏我粋€(gè)return語句前忘記關(guān)閉文件。defer則通過一行代碼解決了這個(gè)問題酝静。
func findHelloWorld(filename string) error {
file, err := os.Open(filename)
if err != nil {
return err
}
defer file.Close()
scanner := bufio.NewScanner(file)
for scanner.Scan() {
if scanner.Text() == "hello, world!" {
return nil
}
}
if err := scanner.Err(); err != nil {
return err
}
return errors.New("Didn't find hello world")
}
乍一看节榜,人們會(huì)認(rèn)為defer明可能會(huì)被編譯器完全優(yōu)化掉。如果我只是在函數(shù)的開頭使用了defer語句别智,編譯器確實(shí)可以通過在每一個(gè)return語句之前插入defer內(nèi)容來實(shí)現(xiàn)全跨。但是實(shí)際情況往往更復(fù)雜。比如亿遂,我們可以在條件語句或者循環(huán)中添加defer浓若。第一種情況可能需要編譯器找到應(yīng)用defer語句的條件分支. 編譯器還需要檢查panic的情況,因?yàn)檫@也是函數(shù)退出執(zhí)行的一種情況蛇数。通過靜態(tài)編譯提供這個(gè)功能(defer)似乎(至少?gòu)谋砻嫔峡矗┦遣惶赡艿摹?/p>
derfer并不是一個(gè)零成本的關(guān)鍵字挪钓,我們可以通過性能測(cè)試來看一下。在下面的測(cè)試中耳舅,我們對(duì)比了一個(gè)互斥鎖在循環(huán)體中加鎖后碌上,直接解鎖以及使用defer語句解鎖的情況。
BenchmarkMutexDeferUnlock-8 20000000 96.6 ns/op
BenchmarkMutexUnlock-8 100000000 19.5 ns/op
使用defer幾乎慢了5倍浦徊。平心而論馏予,77ns也許并不那么重要,但是在一個(gè)循環(huán)中它確實(shí)對(duì)性能產(chǎn)生了影響盔性。通常要由開發(fā)者在性能和代碼的易讀性上做權(quán)衡霞丧。優(yōu)化從來都是需要成本的(譯者注:在Go1.9種defer性能大概提升為50-60ns/op)。
Json與反射
Reflection通常是緩慢的冕香,應(yīng)當(dāng)避免在延遲敏感的服務(wù)中使用蛹尝。JSON是一種常用的數(shù)據(jù)交換格式,但Go的encoding/json庫(kù)依賴于反射來對(duì)json進(jìn)行序列化和反序列化悉尾。使用ffjson(譯者注:easyjson會(huì)更快)突那,我們可以通過使用代碼生成的方式來避免反射的使用,下面是性能對(duì)比构眯。
BenchmarkJSONReflectionMarshal-8 200000 7063 ns/op
BenchmarkJSONMarshal-8 500000 3981 ns/op
BenchmarkJSONReflectionUnmarshal-8 200000 9362 ns/op
BenchmarkJSONUnmarshal-8 300000 5839 ns/op
(ffjson)生成的JSON序列化和反序列化比基于反射的標(biāo)準(zhǔn)庫(kù)速度快38%左右愕难。當(dāng)然,如果我們對(duì)編解碼的性能要求真的很高惫霸,我們應(yīng)該避免使用JSON猫缭。MessagePack是序列化代碼一個(gè)更好的選擇。在這次測(cè)試中我們使用msgp庫(kù)跟JSON的做了對(duì)比它褪。
BenchmarkMsgpackMarshal-8 3000000 555 ns/op
BenchmarkJSONReflectionMarshal-8 200000 7063 ns/op
BenchmarkJSONMarshal-8 500000 3981 ns/op
BenchmarkMsgpackUnmarshal-8 20000000 94.6 ns/op
BenchmarkJSONReflectionUnmarshal-8 200000 9362 ns/op
BenchmarkJSONUnmarshal-8 300000 5839 ns/op
這里的差異是顯著的饵骨。即使是跟(ffjson)生成的代碼相比,MessagePack仍然快很多茫打。
如果我們真的很在意微小的優(yōu)化居触,我們還應(yīng)該避免使用interface類型, 它需要在序列化和反序列化時(shí)做一些額外的處理。在一些動(dòng)態(tài)調(diào)用的場(chǎng)景中老赤,運(yùn)行時(shí)調(diào)用也會(huì)增加一些額外 開銷轮洋。編譯器無法將這些調(diào)用替換為內(nèi)聯(lián)調(diào)用。
BenchmarkJSONReflectionUnmarshal-8 200000 9362 ns/op
BenchmarkJSONReflectionUnmarshalIface-8 200000 10099 ns/op
我們?cè)倏纯凑{(diào)用查找抬旺,即把一個(gè)interface變量轉(zhuǎn)換為它真實(shí)的類型弊予。這個(gè)測(cè)試調(diào)用了同一個(gè)struct的同一個(gè)方法。區(qū)別在于第二個(gè)變量是一個(gè)指向結(jié)構(gòu)體的一個(gè)指針开财。
BenchmarkStructMethodCall-8 2000000000 0.44 ns/op
BenchmarkIfaceMethodCall-8 1000000000 2.97 ns/op
排序是一個(gè)更加實(shí)際的例子汉柒,很好的顯示了性能差異误褪。在這個(gè)測(cè)試中,我們比較排序1,000,000個(gè)結(jié)構(gòu)體和1,000,000個(gè)指向相同結(jié)構(gòu)體的interface碾褂。對(duì)結(jié)構(gòu)體進(jìn)行排序比對(duì)interface進(jìn)行排序快63%兽间。
BenchmarkSortStruct-8 10 105276994 ns/op
BenchmarkSortIface-8 5 286123558 ns/op
總之,如果可能的話避免使用JSON正塌。如果確實(shí)需要用JSON嘀略,生成序列化和反序列化代碼。一般來說乓诽,最好避免依靠反射和interface帜羊,而是編寫使用的具體類型。不幸的是鸠天,這往往導(dǎo)致很多重復(fù)的代碼讼育,所以最好以抽象的這個(gè)代碼生成。再次粮宛,權(quán)衡得失窥淆。
內(nèi)存管理
Go實(shí)際上不暴露堆或直接堆棧分配給用戶。事實(shí)上巍杈,“heap”和“stack”這兩個(gè)詞沒有出現(xiàn)在Go語言規(guī)范的任何地方忧饭。這意味著有關(guān)棧和堆東西只在技術(shù)上實(shí)現(xiàn)相關(guān)。實(shí)際上筷畦,每個(gè)goroutine確實(shí)有著自己堆和棧词裤。編譯器確實(shí)難逃分析,以確定對(duì)象是在棧上還是在堆中分配鳖宾。
不出所料的吼砂,避免堆分配可以成為優(yōu)化的主要方向。通過在棧中分配空間(即多使用A{}的方式創(chuàng)建對(duì)象鼎文,而不是使用new(A)的方式)渔肩,我們避免了昂貴的malloc調(diào)用,如下面所示的測(cè)試拇惋。
BenchmarkAllocateHeap-8 20000000 62.3 ns/op 96 B/op 1 allocs/op
BenchmarkAllocateStack-8 100000000 11.6 ns/op 0 B/op 0 allocs/op
自然周偎,通過指針傳值比通過對(duì)象傳世要快,因?yàn)榍罢咝枰獜?fù)制唯一的一個(gè)指針撑帖,而后者需要復(fù)制整個(gè)對(duì)象蓉坎。下面測(cè)試結(jié)果中的差異幾乎是可以忽略的,因?yàn)檫@個(gè)差異很大程度上取決于被拷貝的對(duì)象的類型胡嘿。注意蛉艾,可能有一些編譯器對(duì)對(duì)這個(gè)測(cè)試進(jìn)行一些編譯優(yōu)化。
BenchmarkPassByReference-8 1000000000 2.35 ns/op
BenchmarkPassByValue-8 200000000 6.36 ns/op
然而,heap空間分配的最大的問題在于GC(垃圾回收)勿侯。如果我們生成了很多生命周期很短的對(duì)象拓瞪,我們會(huì)觸發(fā)GC工作。在這種場(chǎng)景中對(duì)象池就派上用場(chǎng)了罐监。在下面的測(cè)試用吴藻,我們比較了使用堆分配與使用sync.Pool的情況。對(duì)象池提升了5倍的性能弓柱。
BenchmarkConcurrentStructAllocate-8 5000000 337 ns/op
BenchmarkConcurrentStructPool-8 20000000 65.5 ns/op
需要指出的是,Go的sysc.Pool在垃圾回收過程中也會(huì)被回收侧但。使用sync.Pool的作用是復(fù)用垃圾回收操作之間的內(nèi)存矢空。我們也可以維護(hù)自己的空閑對(duì)象列表使對(duì)象不被回收,但這樣可能就讓垃圾回收失去了應(yīng)有的作用禀横。Go的pprof工具在分析內(nèi)存使用情況時(shí)非常有用的屁药。在盲目做內(nèi)存優(yōu)化之前一定要使用它來進(jìn)行分析。
親和緩存
當(dāng)性能真的很重要時(shí)柏锄,你必須開始硬件層次的思考酿箭。著名的一級(jí)方程式車手杰基·斯圖爾特曾經(jīng)說過,“要成為一個(gè)賽車手你不必成為一名工程師趾娃,但你必須有機(jī)械知識(shí)缭嫡。”深刻理解一輛汽車的內(nèi)部工作原理可以讓你成為一個(gè)更好的駕駛員抬闷。同樣妇蛀,理解計(jì)算機(jī)如何工作可以使你成為一個(gè)更好的程序員。例如笤成,內(nèi)存如何布局的评架?CPU緩存如何工作的?硬盤如何工作的炕泳?
內(nèi)存帶寬仍然是現(xiàn)代CPU的有限資源纵诞,因此緩存就顯得極為重要,以防止性能瓶頸∨嘧瘢現(xiàn)在的多核處理器把數(shù)據(jù)緩存在cache line中浙芙,大小通常為64個(gè)字節(jié),以減少開銷較大的主存訪問荤懂。為了保證cache的一致性茁裙,對(duì)內(nèi)存的一個(gè)小小的寫入都會(huì)讓cache line被淘汰。對(duì)相鄰地址的讀操作就無法命中對(duì)應(yīng)的cache line节仿。這種現(xiàn)象叫做false sharing晤锥。 當(dāng)多個(gè)線程訪問同一個(gè)cache line中的不同數(shù)據(jù)時(shí)這個(gè)問題就會(huì)變得很明顯。
想象一下,Go語言中的一個(gè)struct是如何在內(nèi)存中存儲(chǔ)的矾瘾,我們用之前的ring buffer 作為一個(gè)示例女轿,結(jié)構(gòu)體可能是下面這樣:
type RingBuffer struct {
queue uint64
dequeue uint64
mask, disposed uint64
nodes nodes
}
queue和dequeue字段分別用于確定生產(chǎn)者和消費(fèi)者的位置。這些字段的大小都是8byte壕翩,同時(shí)被多個(gè)線程并發(fā)訪問和修改來實(shí)現(xiàn)隊(duì)列的插入和刪除操作蛉迹,因?yàn)檫@些字段在內(nèi)存中是連續(xù)存放的,它們僅僅使用了16byte的內(nèi)存放妈,它們很可能被存放在同一個(gè)cache line中北救。因此修改其中的任何一個(gè)字段都會(huì)導(dǎo)致其它字段緩存被淘汰,也就意味著接下來的讀取操作將會(huì)變慢芜抒。也就是說珍策,在ring buffer中添加和刪除元素會(huì)導(dǎo)致很多的CPU緩存失效。
我們可以給結(jié)構(gòu)體的字段直接增加padding.每一個(gè)padding都跟一個(gè)CPU cache line一樣大宅倒,這樣就能確保ring buffer的字段被緩存在不同的cache line中攘宙。下面是修改后的結(jié)構(gòu)體:
type RingBuffer struct {
_padding0 [8]uint64
queue uint64
_padding1 [8]uint64
dequeue uint64
_padding2 [8]uint64
mask, disposed uint64
_padding3 [8]uint64
nodes nodes
}
實(shí)際運(yùn)行時(shí)會(huì)有多少區(qū)別呢?跟其他的優(yōu)化一樣拐迁,優(yōu)化效果取決于實(shí)際場(chǎng)景蹭劈。它跟CPU的核數(shù)、資源競(jìng)爭(zhēng)的數(shù)量线召、內(nèi)存的布局有關(guān)铺韧。雖然有很多的因素要考慮,我們還是要用數(shù)據(jù)來說話灶搜。我們可以用添加過padding和沒有padding的ring buffer來做一個(gè)對(duì)比祟蚀。
首先,我們測(cè)試一個(gè)生產(chǎn)者和一個(gè)消費(fèi)者的情況割卖,它們分別運(yùn)行在一個(gè)gorouting中.在這個(gè)測(cè)試中前酿,兩者的差別非常小,只有不到15%的性能提升:
BenchmarkRingBufferSPSC-8 10000000 156 ns/op
BenchmarkRingBufferPaddedSPSC-8 10000000 132 ns/op
但是鹏溯,當(dāng)我們有多個(gè)生產(chǎn)者和多個(gè)消費(fèi)者時(shí)罢维,比如各100個(gè),區(qū)別就會(huì)得更加明顯丙挽。在這種情況下肺孵,填充的版本快了約36%。
BenchmarkRingBufferMPMC-8 100000 27763 ns/op
BenchmarkRingBufferPaddedMPMC-8 100000 17860 ns/op
False sharing是一個(gè)非逞詹現(xiàn)實(shí)的問題平窘。根據(jù)并發(fā)和內(nèi)存爭(zhēng)的情況,添加Padding以減輕其影響凳怨。這些數(shù)字可能看起來微不足道的瑰艘,但它已經(jīng)起到優(yōu)化作用了是鬼,特別是在考慮到在時(shí)鐘周期的情況下。
無鎖共享
無鎖的數(shù)據(jù)結(jié)構(gòu)對(duì)充分利用多核心是非常重要的紫新【郏考慮到Go致力于高并發(fā)的使用場(chǎng)景,它不鼓勵(lì)使用鎖芒率。它鼓勵(lì)更多的使用channel而不是互斥鎖囤耳。
這就是說,標(biāo)準(zhǔn)庫(kù)確實(shí)提供了常用的內(nèi)存級(jí)別的原子操作偶芍, 如atomic包充择。它提供了原子比較并交換,原子指針訪問匪蟀。然而聪铺,使用原子包在很大程度上是不被鼓勵(lì)的:
We generally don’t want sync/atomic to be used at all…Experience has shown us again and again that very very few people are capable of writing correct code that uses atomic operations…If we had thought of internal packages when we added the sync/atomic package, perhaps we would have used that. Now we can’t remove the package because of the Go 1 guarantee.
實(shí)現(xiàn)無鎖有多困難?是不是只要用一些CAS實(shí)現(xiàn)就可以了萄窜?在了解了足夠多的知識(shí)后,我認(rèn)識(shí)到這絕對(duì)是一把雙刃劍撒桨。無鎖的代碼實(shí)現(xiàn)起來可能會(huì)非常復(fù)雜查刻。atomic和unsafe包并不易用。而且凤类,編寫線程安全的無鎖代碼非常有技巧性并且很容易出錯(cuò)穗泵。像ring buffer這樣簡(jiǎn)單的無鎖的數(shù)據(jù)結(jié)構(gòu)維護(hù)起來還相對(duì)簡(jiǎn)單,但是其它場(chǎng)景下就很容易出問題了谜疤。
Ctrie是我寫的一篇無鎖的數(shù)據(jù)結(jié)構(gòu)實(shí)現(xiàn)佃延,這里有詳細(xì)介紹盡管理論上很容易理解,但事實(shí)上實(shí)現(xiàn)起來非常復(fù)雜夷磕。復(fù)雜實(shí)現(xiàn)最主要的原因就是缺乏雙重CAS,它可以幫助我們自動(dòng)比較節(jié)點(diǎn)(去檢測(cè)tree上的節(jié)點(diǎn)突變)履肃,也可以幫助我們生成節(jié)點(diǎn)快照。因?yàn)闆]有硬件提供這樣的操作坐桩,需要我們自己去模擬尺棋。
第一個(gè)版本的Ctrie實(shí)現(xiàn)是非常失敗的,不是因?yàn)槲义e(cuò)誤的使用了Go的同步機(jī)制绵跷,而是因?yàn)閷?duì)Go語言做了錯(cuò)誤的假設(shè)膘螟。Ctrie中的每個(gè)節(jié)點(diǎn)都有一個(gè)和它相關(guān)聯(lián)的同伴節(jié)點(diǎn),當(dāng)進(jìn)行快照時(shí)碾局,root節(jié)點(diǎn)都會(huì)被拷貝到一個(gè)新的節(jié)點(diǎn)荆残,當(dāng)樹中的節(jié)點(diǎn)被訪問時(shí),也會(huì)被惰性拷貝到新的節(jié)點(diǎn)(持久化數(shù)據(jù)結(jié)構(gòu)),這樣的快照操作是常數(shù)耗時(shí)的净当。為了避免整數(shù)溢出内斯,我們使用了在堆上分配對(duì)象來區(qū)分新老節(jié)點(diǎn)。在Go語言中,我們使用了空的struct嘿期。在Java中品擎,兩個(gè)新生成的空object是不同的,因?yàn)樗鼈兊膬?nèi)存地址不同备徐,所以我假定了Go的規(guī)則也是一樣的萄传。但是,結(jié)果是殘酷的蜜猾,可以參考下面的文檔:
A struct or array type has size zero if it contains no fields (or elements, respectively) that have a size greater than zero. Two distinct zero-size variables may have the same address in memory.
所以悲劇的事情發(fā)生了秀菱,兩個(gè)新生成的節(jié)點(diǎn)在比較的時(shí)候是相等的,所以雙重CAS總是成功的蹭睡。這個(gè)BUG很有趣衍菱,但是在高并發(fā)、無鎖環(huán)境下跟蹤這個(gè)bug簡(jiǎn)直就是地獄肩豁。如果在使用這些方法的時(shí)候脊串,第一次就沒有正確的使用,那么后面會(huì)需要大量的時(shí)間去解決隱藏的問題清钥,而且也不是你第一次做對(duì)了琼锋,后面就一直是對(duì)的。
但是顯而易見祟昭,編寫復(fù)雜的無鎖算法是有意義的缕坎,否則為什么還會(huì)有人這么做呢?Ctrie跟同步map或者跳躍表比起來篡悟,插入操作更耗時(shí)一些谜叹,因?yàn)閷ぶ凡僮髯兌嗔恕trie真正的優(yōu)勢(shì)是內(nèi)存消耗搬葬,跟大多的Hash表不同荷腊,它總是一系列在tree中的keys。另一個(gè)性能優(yōu)勢(shì)就是它可以在常量時(shí)間內(nèi)完成線性快照踩萎。我們對(duì)比了在100個(gè)并發(fā)的條件下對(duì)synchronized map 和Ctrie進(jìn)行快照:
BenchmarkConcurrentSnapshotMap-8 1000 9941784 ns/op
BenchmarkConcurrentSnapshotCtrie-8 20000 90412 ns/op
在特定的訪問模式下停局,無鎖數(shù)據(jù)結(jié)構(gòu)可以在多線程系統(tǒng)中提供更好的性能。例如香府,NATS消息隊(duì)列使用基于synchronized map的數(shù)據(jù)結(jié)構(gòu)來完成訂閱匹配董栽。如果使用無鎖的Ctrie,吞吐量會(huì)提升很多企孩。下圖中的耗時(shí)中锭碳,藍(lán)線表示使用基于鎖的數(shù)據(jù)結(jié)構(gòu)的實(shí)現(xiàn),紅線表示無鎖的數(shù)據(jù)結(jié)構(gòu)的實(shí)現(xiàn)
在特定的場(chǎng)景中避免使用鎖可以帶來很好的性能提升勿璃。從ring buffer和channel的對(duì)比中就可以看出無鎖結(jié)構(gòu)的明顯優(yōu)勢(shì)擒抛。然而推汽,我們需要在編碼的復(fù)雜程度以及獲得的好處之間進(jìn)行權(quán)衡。事實(shí)上歧沪,有時(shí)候無鎖結(jié)構(gòu)并不能提供任何實(shí)質(zhì)的好處歹撒。
優(yōu)化的注意事項(xiàng)
正如我們從上面的討論所看到的,性能優(yōu)化總是有成本的诊胞。認(rèn)識(shí)和理解優(yōu)化方法僅僅是第一步暖夭。更重要的是理解應(yīng)該在何時(shí)何處取使用它們。引用 C. A. R. Hoare的一句名言撵孤,它已經(jīng)成為了適用所有編程人員的經(jīng)典格言:
The real problem is that programmers have spent far too much time worrying about efficiency in the wrong places and at the wrong times; premature optimization is the root of all evil (or at least most of it) in programming.
但這句話的觀點(diǎn)不是反對(duì)優(yōu)化迈着,而是讓我們學(xué)會(huì)在速度之間進(jìn)行權(quán)衡——算法的速度、響應(yīng)速度邪码、維護(hù)速度以及系統(tǒng)速度裕菠。這是一個(gè)很主觀的話題,而且沒有一個(gè)簡(jiǎn)單的標(biāo)準(zhǔn)闭专。過早進(jìn)行優(yōu)化是錯(cuò)誤的根源嗎奴潘?我是不是應(yīng)該先實(shí)現(xiàn)功能,然后再優(yōu)化影钉?或者是不是根本就不需要優(yōu)化萤彩?沒有標(biāo)準(zhǔn)答案。有時(shí)候先實(shí)現(xiàn)功能再提升速度也是可以的斧拍。
不過,我的建議是只對(duì)關(guān)鍵的路徑進(jìn)行優(yōu)化杖小。你在關(guān)鍵路徑上走的越遠(yuǎn)肆汹,你優(yōu)化的回報(bào)就會(huì)越低以至于近乎是在浪費(fèi)時(shí)間。能夠?qū)π阅苁欠襁_(dá)標(biāo)做出正確的判斷是很重要的予权。不要在此之外浪費(fèi)時(shí)間昂勉。要用數(shù)據(jù)驅(qū)動(dòng)——用經(jīng)驗(yàn)說話,而不是出于一時(shí)興起扫腺。還有就是要注重實(shí)際岗照。給一段時(shí)間不是很敏感的代碼優(yōu)化掉幾十納秒是沒有意義的。比起這個(gè)還有更多需要優(yōu)化的地方笆环。
總結(jié)
如果你已經(jīng)讀到了這里攒至,恭喜你,但你可能還有一些問題搞錯(cuò)了躁劣。我們已經(jīng)了解到在軟件中我們實(shí)際上有兩種速度——響應(yīng)速度和執(zhí)行速度迫吐。用戶想要的是第一種,而開發(fā)者追求的是第二種账忘,CTO則兩者都要志膀。目前第一種速度是最重要的熙宇,只要你想讓用戶用你的產(chǎn)品。第二種速度則是需要你排期和迭代來實(shí)現(xiàn)的溉浙。它們經(jīng)常彼此沖突烫止。
也許更耐人尋味的是,我們討論了一些可以讓Go提升一些性能并讓它在低延時(shí)系統(tǒng)中更可用的方法戳稽。Go語言是為簡(jiǎn)潔而生的馆蠕,但是這種簡(jiǎn)潔有時(shí)候是有代價(jià)的。就跟前面兩種速度的權(quán)衡一樣广鳍,在代碼的可維護(hù)性以及代碼性能上也需要權(quán)衡荆几。速度往往意味著犧牲代碼的簡(jiǎn)潔性、更多的開發(fā)時(shí)間和后期維護(hù)成本赊时。要明智的做出選擇吨铸。
如果您喜歡這篇文章,請(qǐng)點(diǎn)擊喜歡祖秒;如果想及時(shí)獲得最新的咨詢诞吱,請(qǐng)點(diǎn)擊關(guān)注。您的支持是對(duì)作者都是最大的激勵(lì)竭缝,萬分感激房维!By 孫飛