Go:簡單的優化筆記
本文整理譯自 Golang: simple optimization notes:https://medium.com/scum-gazeta/golang-simple-optimization-notes-70bc64673980
在雲計算時代,我們經常創建 Serverless 應用(一種雲原生開發模式,允許開發人員構建和運行應用程序,而無需管理服務器)。當我們的項目採用這種模式,那基礎設施維護預算將排在首位。如果我們的服務負載很低,它實際上近乎是免費的。但是如果出現問題,你將爲此付出很多!當談到金錢時,你肯定會以某方式對它做出反應。
當你的 VPS 運行着多個服務應用,但其中一個有時會佔用所有的資源,以至於都無法通過 ssh 訪問服務器。你轉到使用 Kubernetes 集羣,爲所有應用程序設置限制。隨後看到一些應用程序被重新啓動,因爲 OOM-killer 解決了內存” 泄漏 “問題。
當然, OOM 並不總是泄漏問題,也可能是資源超支。泄漏問題大概率是由程序錯誤引起的,我們今天談論的主題是如何儘量避免這種情況。
過多的資源消耗會傷害錢包,這意味着我們需要立即採取行動。
不要過早優化
現在讓我們談談優化。希望你能明白爲什麼我們不要過早優化!
-
第一,優化可能是無用的工作。因爲我們應該先研究整個應用程序,而你的代碼很可能不會成爲瓶頸。我們需要的是快速的結果,MVP(Minimum Viable Product,最簡可行產品),然後纔會考慮它的問題。
-
第二,優化都必須有所依據。也就是說,每次優化都應該建立在基準上,我們必須證明它給我們帶來了多少利潤。
-
第三,優化也許會帶來複雜。你需要知道的是,大多數優化會使代碼的可讀性變差。你需要把握好這種平衡。
優化建議
現在我們按照 Go 中的標準實體分類,來給出一些實用建議。
1. 數組與切片
提前爲切片分配內存
儘量使用第三個參數:make([]T, 0, len)
如果不知道元素確切的數量並且切片是短暫的,可以分配更大一點,保障切片在運行時不會增長。
不要忘記使用 copy
儘量不要在複製時使用 append,例如在合併兩個或多個切片時。
正確迭代
一個包含許多元素或大元素的切片,使用 for 去獲取單個元素。通過這種方法,將避免不必要的複製。
複用切片
如果對傳入的切片進行某種操作並返回已經修改的結果,我們可以返回它。這樣能避免新的內存分配。
不要留下不使用的切片部分
如果需要從切片中切下一小塊並僅使用它,該切片的主要部分也將被保留。正確的做法是,爲這小塊切片使用新的副本,而將舊的切片扔給 GC。
2. 字符串
正確拼接
如果拼接字符串可以在一個語句中完成,那就使用 +
操作符。如果需要在循環中執行此操作,使用 string.Builder
,並使用它的 Grow
方法預先指定 Builder
的大小,減少內存分配次數。
轉換優化
string 和 []byte 在底層結構上非常相近,有時這兩種類型之間可以通過強轉換來避免內存分配。
字符串駐留
可以池化字符串,從而幫助編譯器只存儲一次相同的字符串。
避免分配
我們可以使用 map(級聯)而不是複合鍵,我們可以使用字節切片。儘量不使用 fmt
包,因爲它所有的方法都用到了反射。
3. 結構體
避免拷貝大結構體
我們理解的小結構體是不超過 4 個字段不超過一個機器字大小。
一些典型的拷貝場景
-
投射到 interface
-
通道的接收和發送
-
替換 map 中的元素
-
向切片添加元素
-
迭代(range)
避免通過指針訪問結構體字段
解引用是昂貴的,我們應該儘可能少地這樣做,尤其是在循環中。同時它也失去了使用快速寄存器的能力。
處理小結構體
這項工作由編輯器進行優化,這意味着它很便宜。
使用對齊減小結構體大小
我們可以對齊結構體(根據字段的大小,以正確的順序排列它們),以此減小結構體本身的大小。
4. 函數
使用內聯函數或自己內聯它們
嘗試編寫可供編譯器內聯的小函數,它會很快,甚至快過自己在函數中嵌入代碼。對於熱路徑(hot path)尤其如此。
哪些不會內聯
-
recovery
-
select 塊
-
類型聲明
-
defer
-
goroutine
-
for-range
合理地選擇函數參數
嘗試使用小參數,因爲它們的複製將被優化。嘗試複製和棧增長在 GC 負載保持平衡。避免大量參數,讓你的程序使用快速寄存器(它們的數量是有限的)。
命名返回值
這似乎比在函數體中聲明這些變量更高效。
保存中間結果
幫助編譯器優化你的代碼,保存中間結果,然後會有更多的選項來優化你的代碼。
仔細地使用 defer
儘量不要使用 defer,或者至少不要在循環中使用它。
助力 hot path
避免在熱路徑分配內存,尤其是短生命對象。製作最常見分支(if,switch)
5. Map
提前分配內存
和 slice 一樣,初始化 map 時,指定其大小。
使用空結構體爲值
struct{} 什麼都不是(不佔內存),因此例如傳遞信號時,使用它是非常有益的。
清空 map
map 只能增長,不能縮小。我們需要重置 map 時,刪除其所有元素是無濟於事的。
儘量不在鍵和值中使用指針
如果 map 中不包含指針,那麼 GC 就不會在上面浪費寶貴的時間。字符串也使用了指針,因此應該使用字節數組而不是字符串作爲鍵。
減少修改次數
同樣,我們不想使用指針,但我們可以使用 map 和 slice 的組合,將鍵存儲在 map 中,將值存在 slice。這樣我們就可以不受限制地更改值。
6. Interface
計算內存分配
請記住,要爲接口分配值時,首先需要將其複製到某處,然後將指針黏貼給它。關鍵是複製。事實證明,接口的裝箱和拆箱的成本將近似於結構體大小的一次分配。
選擇最優類型
在某些情況下,接口的裝箱和拆箱期間沒有分配。例如,變量和常量的小值或布爾值、具有一個簡單字段的結構體、指針(包括 map、channel、func)
避免內存分配
與其他地方一樣,儘量避免不必要的分配。例如將一個接口分配給另一個接口,而不是裝箱兩次。
僅在需要時使用
避免在頻繁調用的函數參數和返回結果中使用接口。我們不需要額外的拆裝包操作。減少使用接口方法調用的頻率,因爲它會阻止內聯。
7. 指針、通道、邊界檢查
避免不必要的解引用
尤其是在循環中,因爲事實證明它太昂貴了。解引用是我們不想自費執行的操作。
使用通道效率低下
channel 同步比其他同步原語方法慢。另外, select 中的 case 越多,我們的程序就越慢。但是,select,case 加 default 有被優化。
避免不必要的邊界檢查
這也很昂貴,我們應該避免它。例如,只檢查(獲取)一次最大切片索引,而不是多次。最好立即嘗試獲得極端選項。
總結
在整篇文章中,我們看到了一些相同的優化規則。
幫助編譯器做出正確的決定,它會感謝你的。在編譯時分配內存,使用中間結果,並儘量保持你的代碼可讀。
我再次重申,對於隱式優化,基準是強制性的。如果因爲編譯器在不同版本之間變化太快,昨天工作的東西明天就不能工作,反之亦然。
不要忘記使用 Go 內置的分析和跟蹤工具。
譯者有話說
注意,作者的建議並不一定是對的。就像原文中有人評價,爲什麼不在每條建議下面列出優化代碼。因爲作者更希望開發人員把這些建議當做一張備忘單,知道這些瓶頸並主動去尋找如何做優化。
機器鈴砍菜刀
歡迎添加小菜刀微信
加入 Golang 分享羣學習交流!
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/0E466avtngc_V_CsIAqyvQ