如何Go的更快

先拋出幾個問題:

  1. string、[]byte 各在什么場景用
  2. sync.pool 用在什么地方?
  3. map、slice 誰效率高?
  4. 反射的效率如何?
  5. 值傳遞還是指針傳遞好

一、定義"快"

很多人對操作系統(tǒng)中的“快”沒什么概念, 那如何知道:

同機房內(nèi) RTT (Round Trip Time)大約是多少?
如果將一個應用內(nèi)的函數(shù)的調(diào)用拆成兩個應用 RPC 調(diào)用,將增加多少延遲?
打印日志有多快,打印日志的多少會增加多少延遲?

我們可以在這個網(wǎng)站看到延遲數(shù)隨著硬件發(fā)展怎么變得更快。2005年之前,cpu和內(nèi)存的發(fā)展很快, 之后是硬盤和網(wǎng)絡(luò)。

如果放大這個時間,把L1緩存讀取時間比如一次心跳時間:

* Minute:
L1 cache reference                  0.5 s         One heart beat (0.5 s)
Branch mispredict(分支預測錯誤)       5 s           Yawn
L2 cache reference                  7 s           Long yawn
Mutex lock/unlock                   25 s          Making a coffee
* Hour:
Main memory reference               100 s         Brushing your teeth
Compress 1K bytes with Zippy        50 min        One episode of a TV show (including ad breaks)
* Day:
Send 2K bytes over 1 Gbps network   5.5 hr        From lunch to end of work day
* Week
SSD random read                     1.7 days      A normal weekend
Read 1 MB sequentially from memory  2.9 days      A long weekend
同一個數(shù)據(jù)中心網(wǎng)絡(luò)上跑一個來(RTT):
Round trip within same datacenter   5.8 days      A medium vacation
Read 1 MB sequentially from SSD    11.6 days      Waiting for almost 2 weeks for a delivery
* Year
Disk seek                           16.5 weeks    A semester in university
Read 1 MB sequentially from disk    7.8 months    Almost producing a new human being
The above 2 together                1 year
*Decade
Send packet CA->Netherlands->CA     4.8 years     Average time it takes to complete a bachelor's degree

你或許會像cpu那樣感嘆: “這個世界太慢了!”

二、如何“快”

USE(Utilization Saturation and Errors)是由 Brendan Gregg提出的 可以分析任何系統(tǒng)性能的方法論,它的思想就是根據(jù)一個 checklis快速找到系統(tǒng)的錯誤和資源(這里的資源主要包括但不限于:CPU,內(nèi)存,網(wǎng)絡(luò),磁盤等)瓶頸。

  • Utilization 使用率:100%的使用率通常是系統(tǒng)性能瓶頸的標志,表示容量已經(jīng)用盡或者全部時間都用于服務。
  • Saturation 飽和度:通常是一些排隊隊列的長度,如任務調(diào)度隊列長度,網(wǎng)絡(luò)發(fā)送隊列長度。100%的使用率通常是系統(tǒng)性能瓶頸的標志。
  • Errors 錯誤:是否有錯誤產(chǎn)生,因為出現(xiàn)錯誤概率最大,其次是錯誤很容易被觀察到。錯誤數(shù)越多,表明系統(tǒng)的問題越嚴重。

有了上面的方法, 我們可以重復以下的流程去找到系統(tǒng)的性能問題,從而達到更快:


USE

有趣的是,Brendan Gregg 在阿波羅飛船的系統(tǒng)設(shè)計中也看到了類似的方法論。

三、“慢”在哪

1、內(nèi)存分配中的堆區(qū)和棧區(qū)

棧(Stack)是一種擁有特殊規(guī)則的線性表


棧(Stack)

棧分配是很廉價的,它只需要兩個CPU指令:一個是分配入棧,另一個是棧內(nèi)釋放。也就是說在棧上分配內(nèi)存,消耗的僅是將數(shù)據(jù)拷貝到內(nèi)存的時間,而內(nèi)存的 I/O 通常能夠達到 30GB/s,因此在棧上分配內(nèi)存效率是非常高的。
而堆分配代價是昂貴的,需要找一塊足夠的空間, 大多不是連續(xù)、不可預知大小的。為此付出的代價是分配速度較慢,而且會形成內(nèi)存碎片。

我們現(xiàn)在現(xiàn)在說的并不是數(shù)據(jù)結(jié)構(gòu)中的堆和棧,內(nèi)存中的棧區(qū)處于相對較高的地址,以地址的增長方向為上的話,棧地址是向下增長的。棧中分配局部變量空間,堆區(qū)是向上增長的用于分配程序員申請的內(nèi)存空間。

程序中的數(shù)據(jù)和變量都會被分配到程序所在的虛擬內(nèi)存中,內(nèi)存空間包含兩個重要區(qū)域:棧區(qū)(Stack)和堆區(qū)(Heap)。函數(shù)調(diào)用的參數(shù)、返回值以及局部變量大都會被分配到棧上,這部分內(nèi)存會由編譯器進行管理;Go 以及 Java 等編程語言會由工程師和編譯器共同管理,堆中的對象由內(nèi)存分配器分配并由垃圾收集器回收。具體的分配原理和實現(xiàn)參考: 內(nèi)存分配器

由于棧上的空間是自動分配自動回收的,所以棧上的數(shù)據(jù)的生存周期只是在函數(shù)的運行過程中,運行后就釋放掉,不可以再訪問。而堆上的數(shù)據(jù)只要程序員不釋放空間,就一直可以訪問到,不過缺點是一旦忘記釋放會造成內(nèi)存泄露。

堆:由于找到的堆結(jié)點的大小不一定正好等于申請的大小,系統(tǒng)會自動的將多余的那部分重新放入空閑鏈表中。也就是說堆會在申請后還要做一些后續(xù)的工作這就會引出申請效率的問題

2、逃逸分析

堆和棧各有優(yōu)缺點,該怎么在編程中處理這個問題呢?C語言中,需要開發(fā)者自己學習如何進行內(nèi)存分配,選用怎樣的內(nèi)存分配方式來適應不同的算法需求。比如,函數(shù)局部變量盡量使用棧,全局變量、結(jié)構(gòu)體成員使用堆分配等。需要花費很長的時間在不同的項目中學習、記憶這些概念并加以實踐和使用。慶幸的是Go語言將這個過程整合到了編譯器中,命名為“變量逃逸分析”。通過編譯器分析代碼的特征和代碼的生命周期,決定應該使用堆還是棧來進行內(nèi)存分配。

編譯器覺得變量應該分配在堆和棧上的原則是:
1.變量是否被取地址;
2.變量是否發(fā)生逃逸。
Go逃逸分析最基本的原則是:如果一個函數(shù)返回對一個變量的引用,那么它就會發(fā)生逃逸

來看看棧逃逸的幾種情況:

在Go中通過逃逸分析日志來確定變量是否逃逸,開啟逃逸分析日志:
go run -gcflags '-m -l' main.go
-m 會打印出逃逸分析的優(yōu)化策略,實際上最多總共可以用 4 個 -m,但是信息量較大,一般用 1 個就可以了。
-l 會禁用函數(shù)內(nèi)聯(lián),在這里禁用掉內(nèi)聯(lián)能更好的觀察逃逸情況,減少干擾。

【舉例】

a).指針逃逸
package main

type Student struct {
    Name string
    Age  int
}

func StudentRegister(name string, age int) *Student {
    s := new(Student) //逃逸

    s.Name = name
    s.Age = age

    return s
}

func main() {
    StudentRegister("Jim", 18)
}
  • new(Student) escapes to heap
b).interface{} 動態(tài)類型逃逸 (不確定長度大?。?/h5>
type User struct {
    name interface{}
}

func main() {
    name := "WilburXu"
    MyPrintln(name)
}

func MyPrintln(one interface{}) (n int, err error) {
    var userInfo = new(User)
    userInfo.name = one // 泛型賦值 逃逸
    return
}

  • ./main.go:124:20: new(User) does not escape
  • ./main.go:120:11: name escapes to heap
func main() { //動態(tài)分配不定空間
    // 在 interface 類型上調(diào)用方法
    a:=1
    fmt.Println(a)
    // 堆 動態(tài)分配不定空間 逃逸
    b := 20
    c := make([]int, 0, b)
}
  • ./main.go:106:13: a escapes to heap
  • ./main.go:109:11: make([]int, 0, b) escapes to heap
c). 棧空間不足逃逸(空間開辟過大)
func Slice() {
    s := make([]int, 1, 8192) // 8k = 8192 字節(jié)

    for index, _ := range s {
        s[index] = index
    }
}

func main() { //??臻g不足逃逸(空間開辟過大)
    Slice()
}
  • make([]int, 1, 8192) escapes to heap

總結(jié):
1、指針逃逸 - 方法返回局部變量指針,就形成變量逃逸
2、??臻g不足逃逸 - 當切片長度擴大到10000時就會逃逸,實際上當??臻g不足以存放當前對象或無法判斷當前切片長時會將對象分配到堆中
3、動態(tài)類型逃逸 - 編譯期間很難確定其參數(shù)的具體類型,也能產(chǎn)生逃逸度
4、閉包引用對象逃逸 - 原本屬于局部變量,由于閉包的引用,不得不放到堆上,以致產(chǎn)生逃逸
5、跨協(xié)程引用對象逃逸 - 原本屬于A協(xié)程的變量,通過指針傳遞給B協(xié)程使用,產(chǎn)生逃逸

四、“更快” 見效

1、性能分析

回到提問,用引用還是值傳遞,我們使用性能分析工具看看以下這個例子:
【舉例】
https://blog.crazytaxii.com/posts/golang_struct_pointer_vs_copy/ (偷懶借用下)

結(jié)論:
什么時候適合用指針

傳值會拷貝整個對象,而傳指針只會拷貝指針地址,指向的對象是同一個。 傳指針可以減少值的拷貝,但是會導致內(nèi)存分配逃逸到堆中,增加垃圾回收(GC)的負擔。對象頻繁創(chuàng)建和刪除的場景下,傳遞指針導致的 GC 開銷可能會嚴重影響性能。

一般情況下,對于需要修改原對象值,或占用內(nèi)存比較大的結(jié)構(gòu)體,選擇傳指針。對于只讀的占用內(nèi)存較小的結(jié)構(gòu)體,直接傳值能夠獲得更好的性能。

需要注意的:

  • 如果需要修改外部變量的值,我們需要使用指針;
  • 不需要對map,slice等引用類型使用指針,因為他們本身就是一個指針
  • 如果有超級大的結(jié)構(gòu)體需要作為函數(shù)的參數(shù),使用指針可以節(jié)省內(nèi)存開銷;
  • 因為指針可以修改其指向數(shù)據(jù)的值,所以最好不要隨意在并發(fā)場景下使用;(map)

2、GC

編程語言的內(nèi)存管理系統(tǒng)除了負責堆內(nèi)存的分配之外,它還需要負責回收不再使用的對象和內(nèi)存空間。golang的垃圾回收采用的是 標記-清理(Mark-and-Sweep) 算法 。關(guān)于內(nèi)存回收這部分可以參考: 7.2 垃圾收集器

GC過程不斷演進為:(并發(fā))標記、(并發(fā))清理、STW三個階段。

Go1.3-Go1.5 GC時間演進

STW(stop the world), GC的一些階段需要停止所有的mutator(應用代碼)以確定當前的引用關(guān)系. 這便是很多人對GC擔心的來源, 這也是GC算法優(yōu)化的重點. 對于大多數(shù)API/RPC服務, 10-20ms左右的STW完全接受的. Golang GC的STW時間從最初的秒級到百ms, 到1.15版本三色標記:10ms級別, 1.7的ms級別, 到現(xiàn)在的1ms以下, 已經(jīng)達到了準實時的程度.

Go1.3-Go1.5 GC時間演進

Go1.4-Go1.8 STW時間
棧大小與STW時間關(guān)系
  • 可以看出STW時間基本都在1ms以下, 有小部分超過2ms, 也比較正常.

然而GC的標記過程仍然會占用比較大的系統(tǒng)開銷(cpu時間)

cup消耗

由上可以看出, Golang GC Mark消耗的CPU時間與存活的對象數(shù)基本成正比(更準確的說是和需掃描的字節(jié)數(shù), 每個對象第一個字節(jié)到對象的最后一個指針字段). 對于G級別以上的存活對象, 掃描一次需要花秒以上的CPU時間.

3、GC分析

拿問答服務舉例:
GODEBUG=gctrace=1 go run ./vipask.go

gc 25 @0.101s 7%: 0.064+0.89+0.013 ms clock, 0.51+0.20/1.2/1.4+0.11 ms cpu, 4->4->1 MB, 5 MB goal, 8 P

解讀:
第25次運行,進程已經(jīng)啟動0.101s 本次執(zhí)行占用的進程cpu7% 本次gc耗時:清掃時間0.064ms,并發(fā)標記時間:0.89ms,STW時間0.013ms。 本次gc占用的cpu時間 ... 。堆的大小4mb,gc堆大小4 ,存活對大小1. 整體堆大小5mb, cpu核數(shù)8.

gc 26 @0.108s 6%: 0.031+0.65+0.016 ms clock, 0.24+0.046/0.77/1.2+0.13 ms cpu, 4->4->1 MB, 5 MB goal, 8 P
gc 27 @0.116s 6%: 0.030+0.61+0.038 ms clock, 0.24+0.085/0.93/1.5+0.31 ms cpu, 4->4->1 MB, 5 MB goal, 8 P
gc 28 @0.124s 6%: 0.035+0.58+0.019 ms clock, 0.28+0.17/0.81/1.7+0.15 ms cpu, 4->4->1 MB, 5 MB goal, 8 P
gc 29 @0.131s 5%: 0.025+0.60+0.011 ms clock, 0.20+0.077/0.87/1.7+0.092 ms cpu, 4->4->2 MB, 5 MB goal, 8 P
gc 30 @0.137s 5%: 0.024+0.53+0.010 ms clock, 0.19+0.087/0.82/1.5+0.085 ms cpu, 4->4->2 MB, 5 MB goal, 8 P
gc 31 @0.145s 5%: 0.050+1.1+0.002 ms clock, 0.40+0.11/1.7/2.7+0.023 ms cpu, 4->5->2 MB, 5 MB goal, 8 P
gc 32 @0.156s 5%: 0.026+0.80+0.001 ms clock, 0.21+0.13/1.2/2.2+0.013 ms cpu, 5->5->2 MB, 6 MB goal, 8 P
gc 33 @0.165s 5%: 0.043+0.81+0.008 ms clock, 0.35+0.16/1.2/3.0+0.068 ms cpu, 5->5->2 MB, 6 MB goal, 8 P
gc 34 @0.174s 5%: 0.021+1.0+0.017 ms clock, 0.17+0.11/1.5/3.1+0.13 ms cpu, 5->6->3 MB, 6 MB goal, 8 P
gc 35 @0.186s 4%: 0.051+1.0+0.002 ms clock, 0.41+0.11/1.7/3.8+0.020 ms cpu, 6->6->3 MB, 7 MB goal, 8 P
gc 36 @0.197s 4%: 0.033+0.83+0.014 ms clock, 0.26+0.70/1.4/3.3+0.11 ms cpu, 6->7->3 MB, 7 MB goal, 8 P
gc 37 @0.207s 4%: 0.035+1.2+0.003 ms clock, 0.28+0.45/2.0/3.9+0.031 ms cpu, 6->7->3 MB, 7 MB goal, 8 P
gc 38 @0.221s 4%: 0.031+0.96+0.002 ms clock, 0.24+0.10/1.6/3.7+0.016 ms cpu, 7->7->4 MB, 8 MB goal, 8 P
gc 39 @0.236s 4%: 0.027+1.7+0.002 ms clock, 0.22+0/1.6/2.9+0.017 ms cpu, 8->8->4 MB, 9 MB goal, 8 P
gc 40 @0.252s 4%: 0.033+1.7+0.002 ms clock, 0.27+0.054/3.1/4.2+0.022 ms cpu, 8->9->5 MB, 9 MB goal, 8 P
gc 41 @0.272s 4%: 0.048+1.6+0.001 ms clock, 0.38+0.085/2.9/5.6+0.015 ms cpu, 9->10->5 MB, 10 MB goal, 8 P
gc 42 @0.296s 4%: 0.20+2.3+0.020 ms clock, 1.6+2.4/4.0/1.7+0.16 ms cpu, 10->12->7 MB, 11 MB goal, 8 P
.
.
.
gc 52 @120.920s 0%: 0.41+3.2+0.002 ms clock, 3.3+0/6.1/5.1+0.022 ms cpu, 10->10->6 MB, 20 MB goal, 8 P
gc 53 @241.044s 0%: 0.11+4.5+0.004 ms clock, 0.92+0/8.8/7.5+0.035 ms cpu, 6->6->5 MB, 12 MB goal, 8 P
gc 54 @361.077s 0%: 0.096+4.4+0.004 ms clock, 0.77+0/8.4/8.0+0.036 ms cpu, 5->5->5 MB, 11 MB goal, 8 P

1.兩分鐘自動強制一次
2.小于4mb 不會觸發(fā)
3.超過上一次剩余的一倍時觸發(fā)

4、結(jié)合到實際項目中

a). 資源中臺的優(yōu)化方案
gc條件中,把最小內(nèi)存4Mb調(diào)整到40Mb,減少GC次數(shù)

b). 資源中臺中為什么沒用使用并發(fā)請求
go-routine消耗cpu資源

五、 不疾而速

不要過度優(yōu)化,
不要提前優(yōu)化
不要為了優(yōu)化犧牲代碼可讀性
(從go的發(fā)展中也可以看出, 不論是內(nèi)存分配和是gc都是不斷的演進和完善中)


結(jié)合分享再回來思考這些問題:

  1. string、[]byte 各在什么場景用

  2. sync.pool 用在什么地方?

  3. map、slice 誰效率高?

  4. 反射的效率如何?

  5. range怎樣用最優(yōu)

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容