Go:我應該用指針替代結構體的副本嗎?


Go:我應該用指針替代結構體的副本嗎?

logo

對於許多 golang 開發者來說,考慮到性能,最佳實踐是系統地使用指針而非結構體副本。

我們將回顧兩個用例,來理解使用指針而非結構體副本的影響。

1. 數據分配密集型

讓我們舉一個簡單的例子,說明何時要為使用值而共享結構體:

<code>type S struct {
a, b, c int64
d, e, f string
g, h, i float64
}
/<code>

這是一個可以由副本或指針共享的基本結構體:

<code>func byCopy() S {
return S{
a: 1, b: 1, c: 1,
e: "foo", f: "foo",
g: 1.0, h: 1.0, i: 1.0,
}
}

func byPointer() *S {
return &S{
a: 1, b: 1, c: 1,
e: "foo", f: "foo",
g: 1.0, h: 1.0, i: 1.0,
}
}
/<code>

基於這兩種方法,我們現在可以編寫兩個基準測試,其中一個是通過副本傳遞結構體的:

<code>func BenchmarkMemoryStack(b *testing.B) {
var s S

f, err := os.Create("stack.out")
if err != nil {
panic(err)
}
defer f.Close()

err = trace.Start(f)
if err != nil {
panic(err)
}

for i := 0; i < b.N; i++ {
s = byCopy()
}

trace.Stop()

b.StopTimer()

_ = fmt.Sprintf("%v", s.a)
}
/<code>

另一個非常相似,它通過指針傳遞:

<code>func BenchmarkMemoryHeap(b *testing.B) {
var s *S

f, err := os.Create("heap.out")
if err != nil {
panic(err)
}
defer f.Close()

err = trace.Start(f)
if err != nil {
panic(err)
}

for i := 0; i < b.N; i++ {
s = byPointer()
}

trace.Stop()

b.StopTimer()

_ = fmt.Sprintf("%v", s.a)
}
/<code>

讓我們運行基準測試:

<code>go test ./... -bench=BenchmarkMemoryHeap -benchmem -run=^$ -count=10 > head.txt && benchstat head.txt
go test ./... -bench=BenchmarkMemoryStack -benchmem -run=^$ -count=10 > stack.txt && benchstat stack.txt
/<code>

以下是統計數據:

<code>name          time/op
MemoryHeap-4 75.0ns ± 5%
name alloc/op
MemoryHeap-4 96.0B ± 0%
name allocs/op
MemoryHeap-4 1.00 ± 0%
------------------
name time/op
MemoryStack-4 8.93ns ± 4%
name alloc/op
MemoryStack-4 0.00B
name allocs/op
MemoryStack-4 0.00
/<code>

在這裡,使用結構體副本比指針快 8 倍。

為了理解原因,讓我們看看追蹤生成的圖表:

Go:我應該用指針替代結構體的副本嗎?

Go:我應該用指針替代結構體的副本嗎?

第一張圖非常簡單。由於沒有使用堆,因此沒有垃圾收集器,也沒有額外的 goroutine。對於第二張圖,使用指針迫使 go 編譯器將變量逃逸到堆[1],由此增大了垃圾回收器的壓力。如果我們放大圖表,我們可以看到,垃圾回收器佔據了進程的重要部分。

Go:我應該用指針替代結構體的副本嗎?

在這張圖中,我們可以看到,垃圾回收器每隔 4ms 必須工作一次。如果我們再次縮放,我們可以詳細瞭解正在發生的事情:

Go:我應該用指針替代結構體的副本嗎?

藍色,粉色和紅色是垃圾收集器的不同階段,而棕色的是與堆上的分配相關(在圖上標有 “runtime.bgsweep”):

清掃是指回收與堆內存中未標記為使用中的值相關聯的內存。當應用程序 Goroutines嘗試在堆內存中分配新值時,會觸發此活動。清掃的延遲被添加到在堆內存中執行分配的成本中,並且與垃圾收集相關的任何延遲沒有關係。

Go 中的垃圾回收:第一部分 - 基礎[2]

即使這個例子有點極端,我們也可以看到,與棧相比,在堆上為變量分配內存是多麼消耗資源。在我們的示例中,與在堆上分配內存並共享指針相比,代碼在棧上分配結構體並複製副本要快得多。

如果你不熟悉堆棧或堆,如果你想更多地瞭解棧或堆的內部細節,你可以在網上找到很多資源,比如 Paul Gribble 的這篇文章[3]

如果我們使用 GOMAXPROCS = 1 將處理器限制為 1,情況會更糟:

<code>name        time/op
MemoryHeap 114ns ± 4%
name alloc/op
MemoryHeap 96.0B ± 0%
name allocs/op
MemoryHeap 1.00 ± 0%
------------------
name time/op
MemoryStack 8.77ns ± 5%
name alloc/op
MemoryStack 0.00B
name allocs/op
MemoryStack 0.00
/<code>

如果棧上分配的基準數據不變,則堆上的基準從 75ns/op 降低到 114ns/op。

2.方法調用密集型

對於第二個用例,我們將在結構體中添加兩個空方法,稍微調整一下我們的基準測試:

<code>func (s S) stack(s1 S) {}

func (s *S) heap(s1 *S) {}
/<code>

在棧上分配的基準測試將創建一個結構體並通過複製副本傳遞它:

<code>func BenchmarkMemoryStack(b *testing.B) {
var s S
var s1 S

s = byCopy()
s1 = byCopy()
for i := 0; i < b.N; i++ {
for i := 0; i < 1000000; i++ {
s.stack(s1)
}
}
}
/<code>

堆的基準測試將通過指針傳遞結構體:

<code>func BenchmarkMemoryHeap(b *testing.B) {
var s *S
var s1 *S

s = byPointer()
s1 = byPointer()
for i := 0; i < b.N; i++ {
for i := 0; i < 1000000; i++ {
s.heap(s1)
}
}
}
/<code>

正如預期的那樣,結果現在大不相同:

<code>name          time/op
MemoryHeap-4 301µs ± 4%
name alloc/op
MemoryHeap-4 0.00B
name allocs/op
MemoryHeap-4 0.00
------------------
name time/op
MemoryStack-4 595µs ± 2%
name alloc/op
MemoryStack-4 0.00B
name allocs/op
MemoryStack-4 0.00
/<code>

結論

在 go 中使用指針而不是結構體的副本並不總是好事。為了能為你的數據選擇好的語義,我強烈建議您閱讀 Bill Kennedy[4] 撰寫的關於值/指針語義的文章[5]。它將為你提供更好的視角來決定使用自定義類型或內置類型時的策略。

此外,內存使用情況分析肯定會幫助你弄清楚你的內存分配和堆上發生了什麼。


via: https://medium.com/@blanchon.vincent/go-should-i-use-a-pointer-instead-of-a-copy-of-my-struct-44b43b104963

作者:Vincent Blanchon[6]譯者:DoubleLuck[7]校對:magichan[8]

本文由 GCTT[9] 原創編譯,Go 中文網[10] 榮譽推出

[1]

將變量逃逸到堆: https://golang.org/doc/faq#stack_or_heap

[2]

Go 中的垃圾回收:第一部分 - 基礎: https://mp.weixin.qq.com/s/mYp3QbdWR4HEZimFUw9bAA

[3]

這篇文章: https://www.gribblelab.org/CBootCamp/7_Memory_Stack_vs_Heap.html

[4]

Bill Kennedy: https://twitter.com/goinggodotnet

[5]

關於值/指針語義的文章: https://mp.weixin.qq.com/s/3RooAE5KyHgzchiuBFS0iw

[6]

Vincent Blanchon: https://medium.com/@blanchon.vincent

[7]

DoubleLuck: https://github.com/DoubleLuck

[8]

magichan: https://github.com/magichan

[9]

GCTT: https://github.com/studygolang/GCTT

[10]

Go 中文網: https://studygolang.com/


分享到:


相關文章: