還咋優化?我是說 Go 程序
Go 語言是一個極容易上手的語言,而且 Go 程序的優化套路基本上被大家莫得清清楚楚的,如果你有心,可以在互聯網上搜出很多 Go 程序優化的技巧,有些文章可能只介紹了幾個優化點,有些文章從 CPU 的架構到 Slice 預分配,再到通過 pprof 找性能的瓶頸點等等全面介紹 Go 程序的優化,所以說可見的手段基本上被大家摸得門清,最近老貘出了一道題,如下所示,可以看到大家對 Go 語言的優化已經摸的多深了。
const N = 1000
var a [N]int
//go:noinline
func g0(a *[N]int) {
for i := range a {
a[i] = i // line 12
}
}
//go:noinline
func g1(a *[N]int) {
_ = *a // line 18
for i := range a {
a[i] = i // line 20
}
}
Go 官方也沒閒着。雖然 Go 語言創立之初也並沒有目標要和 C++ 語言打平性能,但是 Go 團隊對 Go 語言的編譯和運行時優化也一直在進行着。
最近,Go 語言也正在新加兩個性能優化的特性,一個是 cmd/compile: profile-guided optimization[1], 這個提案 [2] 已經被接受, 後續功能初步成型後我們再介紹。另外一個增加 memory arena[3]。
除了大家常見的通用語言的優化外,影響 Go 程序性能最大的問題之一就是垃圾回收,所以使用 C++、Rust 開發的程序員 diss Go 程序的原因之一。不過這也是垃圾回收編程語言無法繞開的特性,基本上無可避免的帶有 STW 的開銷,即使沒有 STW, 垃圾回收時也會耗資源進行對象的便利和檢查,所以理論上來說 Go 性能相比較 C+/Rust 語言性能總會差一些,除非你禁用垃圾回收、純粹做 CPU 計算。
Debian 的 benchmark's game 網站測試和公佈了好多語言的一些場景的性能比較,比如下面這個是 Rust 和 Go 的幾個實現版本的性能比較:
可以看到在這個二叉樹的場景下 Go 的性能比 Rust 的也差很多。不過性能最好的 Rust 實現使用arena
的內存分配:
use bumpalo::Bump;
use rayon::prelude::*;
#[derive(Debug, PartialEq, Clone, Copy)]
struct Tree<'a> {
left: Option<&'a Tree<'a>>,
right: Option<&'a Tree<'a>>,
}
fn item_check(tree: &Tree) -> i32 {
if let (Some(left), Some(right)) = (tree.left, tree.right) {
1 + item_check(right) + item_check(left)
} else {
1
}
}
fn bottom_up_tree<'r>(arena: &'r Bump, depth: i32) -> &'r Tree<'r> {
let tree = arena.alloc(Tree { left: None, right: None });
if depth > 0 {
tree.right = Some(bottom_up_tree(arena, depth - 1));
tree.left = Some(bottom_up_tree(arena, depth - 1));
}
tree
}
...
arena 是一個內存池的技術,一般來說 arena 會創建一個大的連續內存塊,該內存塊只需要預先分配一次,在這塊內存上的創建和釋放都是手工執行的。
Go 語言準備新加入 arena 的功能,並在標準庫提供一個新的包: arena
。當前這個提案 [4] 還是 holding 的狀態,但是相關的代碼已經陸陸續續地提到 master 分支了,所以說配批准也基本跑不了了,應該在 Go 1.20,也就是明年春季的版本中嘗試使用了。(當然也有開發者對 Go 的這種做法不滿,因爲外部開發者提出這種想法基本上被駁回或者不被關注,而 Go 團隊的人有這想法就可以立馬實現,甚至提案還沒批准)。
包arena
當前提供了幾個方法:
-
NewArena(): 創建一個 Arena, 你可以創建多個 Arena, 批量創建一批對象,統一手工釋放。它不是線程安全的。
-
Free(): 釋放 Arena 以及它上面創建出來的所有的對象。釋放的對象你不應該再使用了,否則可能會導致意想不到的錯誤。
-
New[T any](a *Arena "T any") *T:創建一個對象
-
MakeSlice[T any](a *Arena, len, cap int "T any") []T: 在 Arena 創建一個 Slice。
-
Clone[T any](s T "T any"): 克隆一個 Arena 上對象,只能是指針、slice 或者字符串。如果傳入的對象不是在 Arena 分配的,直接原對象返回,否則脫離 Arena 創建新的對象。
當前還沒有實現MakeMap
、MakeChan
這樣在 Arena 上創建 map 和 channel 的方法,後續可能會加上。
arena 的功能爲一組 Go 對象創建一塊內存,手工整體一次性的釋放,可以避免垃圾回收。畢竟,我們也提到了,垃圾回收是 Go 程序的最大的性能殺手之一。
官方建議在批量創建大量的 Go 對象的時候,每次能以 Mib 分配內存的場景下使用更有效,甚至他們找到了一個場景:protobuf 的反序列化。
因爲涉及到垃圾回收、內存分配的問題,所以這個功能實現起來也並不簡單,涉及到對運行時代碼的改造。不考慮垃圾回收對 arena 的處理, arena 主要的實現在在運行時的 arena.go[5] 中。因爲這個功能還在開發之中,或許這個文件還會有變更。
接下來,我們使用 debian benchmark's game 的二叉樹的例子,對使用 arena 和不使用 arena 的情況下做一個比較:
package main
import (
"arena"
"flag"
"fmt"
"strconv"
"time"
)
// gotip run -tags "goexperiment.arenas" main.go -arena 21
// GOEXPERIMENT=arenas gotip run main.go -arena 21
var n = 0
type Node struct {
left, right *Node
value []byte
}
func bottomUpTree(depth int) *Node {
if depth <= 0 {
return &Node{}
}
return &Node{bottomUpTree(depth - 1), bottomUpTree(depth - 1), make([]byte, 128, 128)}
}
func bottomUpTreeWithArena(depth int, a *arena.Arena) *Node {
node := arena.New[Node](a "Node")
node.value = arena.MakeSlice[byte](a, 128, 128 "byte")
if depth <= 0 {
return node
}
node.left = bottomUpTreeWithArena(depth-1, a)
node.right = bottomUpTreeWithArena(depth-1, a)
return node
}
func (n *Node) itemCheck() int {
if n.left == nil {
return 1
}
return 1 + n.left.itemCheck() + n.right.itemCheck()
}
const minDepth = 4
var useArena = flag.Bool("arena", false, "use arena")
func main() {
flag.Parse()
if flag.NArg() > 0 {
n, _ = strconv.Atoi(flag.Arg(0))
}
appStart := time.Now()
defer func() {
fmt.Printf("benchmark took: %v\n", time.Since(appStart))
}()
if *useArena {
maxDepth := n
if minDepth+2 > n {
maxDepth = minDepth + 2
}
stretchDepth := maxDepth + 1
a := arena.NewArena()
start := time.Now()
check := bottomUpTreeWithArena(stretchDepth, a).itemCheck()
a.Free()
fmt.Printf("stretch tree of depth %d\t check: %d, took: %v\n", stretchDepth, check, time.Since(start))
a = arena.NewArena()
longLiveStart := time.Now()
longLivedTree := bottomUpTreeWithArena(maxDepth, a)
defer a.Free()
for depth := minDepth; depth <= maxDepth; depth += 2 {
iterations := 1 << uint(maxDepth-depth+minDepth)
check = 0
start := time.Now()
for i := 1; i <= iterations; i++ {
a := arena.NewArena()
check += bottomUpTreeWithArena(depth, a).itemCheck()
a.Free()
}
fmt.Printf("%d\t trees of depth %d\t check: %d, took: %v\n", iterations, depth, check, time.Since(start))
}
fmt.Printf("long lived tree of depth %d\t check: %d, took: %v\n", maxDepth, longLivedTree.itemCheck(), time.Since(longLiveStart))
} else {
maxDepth := n
if minDepth+2 > n {
maxDepth = minDepth + 2
}
stretchDepth := maxDepth + 1
start := time.Now()
check := bottomUpTree(stretchDepth).itemCheck()
fmt.Printf("stretch tree of depth %d\t check: %d, took: %v\n", stretchDepth, check, time.Since(start))
longLiveStart := time.Now()
longLivedTree := bottomUpTree(maxDepth)
for depth := minDepth; depth <= maxDepth; depth += 2 {
iterations := 1 << uint(maxDepth-depth+minDepth)
check = 0
start := time.Now()
for i := 1; i <= iterations; i++ {
check += bottomUpTree(depth).itemCheck()
}
fmt.Printf("%d\t trees of depth %d\t check: %d, took: %v\n", iterations, depth, check, time.Since(start))
}
fmt.Printf("long lived tree of depth %d\t check: %d, took: %v\n", maxDepth, longLivedTree.itemCheck(), time.Since(longLiveStart))
}
}
這段程序中我們使用-arena
參數控制要不要使用arena
。首先你必須安裝或者更新gotip
到最新版 (如果你已經安裝了 gotip, 執行gotip downloamd
, 如果還未安裝,請先go install golang.org/dl/gotip@latest
)。
-
啓用
-arena
: 運行GOEXPERIMENT=arenas gotip run -arena main.go 21
-
不啓用
-arena
: 運行GOEXPERIMENT=arenas gotip run -arena=false main.go 21
不過這個特性還在開發之中,功能還不完善。
我在 MacOS 上測試,使用arena
性能會有明顯的提升,而在 windows 下測試,性能反而下降了。
參考資料
[1]
cmd/compile: profile-guided optimization: https://github.com/golang/go/issues/55022#
[2]
提案: https://github.com/golang/proposal/blob/master/design/55022-pgo-implementation.md
[3]
memory arena: https://github.com/golang/go/issues/51317
[4]
提案: https://github.com/golang/go/issues/51317
[5]
arena.go: https://github.com/golang/go/blob/26b48442569102226baba1d9b4a83aaee3d06611/src/runtime/arena.go
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/VtDR2uJ8SQ2jiIS2o2uyfA