Go:簡單的優化筆記

本文整理譯自 Golang: simple optimization notes:https://medium.com/scum-gazeta/golang-simple-optimization-notes-70bc64673980

在雲計算時代,我們經常創建 Serverless 應用(一種雲原生開發模式,允許開發人員構建和運行應用程序,而無需管理服務器)。當我們的項目採用這種模式,那基礎設施維護預算將排在首位。如果我們的服務負載很低,它實際上近乎是免費的。但是如果出現問題,你將爲此付出很多!當談到金錢時,你肯定會以某方式對它做出反應。

當你的 VPS 運行着多個服務應用,但其中一個有時會佔用所有的資源,以至於都無法通過 ssh 訪問服務器。你轉到使用 Kubernetes 集羣,爲所有應用程序設置限制。隨後看到一些應用程序被重新啓動,因爲 OOM-killer 解決了內存” 泄漏 “問題。

當然, OOM 並不總是泄漏問題,也可能是資源超支。泄漏問題大概率是由程序錯誤引起的,我們今天談論的主題是如何儘量避免這種情況。

過多的資源消耗會傷害錢包,這意味着我們需要立即採取行動。

不要過早優化

現在讓我們談談優化。希望你能明白爲什麼我們不要過早優化!

優化建議

現在我們按照 Go 中的標準實體分類,來給出一些實用建議。

1. 數組與切片

提前爲切片分配內存

儘量使用第三個參數:make([]T, 0, len)

如果不知道元素確切的數量並且切片是短暫的,可以分配更大一點,保障切片在運行時不會增長。

不要忘記使用 copy

儘量不要在複製時使用 append,例如在合併兩個或多個切片時。

正確迭代

一個包含許多元素或大元素的切片,使用 for 去獲取單個元素。通過這種方法,將避免不必要的複製。

複用切片

如果對傳入的切片進行某種操作並返回已經修改的結果,我們可以返回它。這樣能避免新的內存分配。

不要留下不使用的切片部分

如果需要從切片中切下一小塊並僅使用它,該切片的主要部分也將被保留。正確的做法是,爲這小塊切片使用新的副本,而將舊的切片扔給 GC。

2. 字符串

正確拼接

如果拼接字符串可以在一個語句中完成,那就使用 + 操作符。如果需要在循環中執行此操作,使用 string.Builder,並使用它的 Grow 方法預先指定 Builder 的大小,減少內存分配次數。

轉換優化

string 和 []byte 在底層結構上非常相近,有時這兩種類型之間可以通過強轉換來避免內存分配。

字符串駐留

可以池化字符串,從而幫助編譯器只存儲一次相同的字符串。

避免分配

我們可以使用 map(級聯)而不是複合鍵,我們可以使用字節切片。儘量不使用 fmt 包,因爲它所有的方法都用到了反射。

3. 結構體

避免拷貝大結構體

我們理解的小結構體是不超過 4 個字段不超過一個機器字大小。

一些典型的拷貝場景

避免通過指針訪問結構體字段

解引用是昂貴的,我們應該儘可能少地這樣做,尤其是在循環中。同時它也失去了使用快速寄存器的能力。

處理小結構體

這項工作由編輯器進行優化,這意味着它很便宜。

使用對齊減小結構體大小

我們可以對齊結構體(根據字段的大小,以正確的順序排列它們),以此減小結構體本身的大小。

4. 函數

使用內聯函數或自己內聯它們

嘗試編寫可供編譯器內聯的小函數,它會很快,甚至快過自己在函數中嵌入代碼。對於熱路徑(hot path)尤其如此。

哪些不會內聯

合理地選擇函數參數

嘗試使用小參數,因爲它們的複製將被優化。嘗試複製和棧增長在 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