new 结构体指针_Go:我应该用指针替代结构体的副本吗?
logo
對于許多 golang 開發者來說,考慮到性能,最佳實踐是系統地使用指針而非結構體副本。
我們將回顧兩個用例,來理解使用指針而非結構體副本的影響。
1. 數據分配密集型
讓我們舉一個簡單的例子,說明何時要為使用值而共享結構體:
type S struct { a, b, c int64 d, e, f string g, h, i float64}這是一個可以由副本或指針共享的基本結構體:
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, }}基于這兩種方法,我們現在可以編寫兩個基準測試,其中一個是通過副本傳遞結構體的:
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)}另一個非常相似,它通過指針傳遞:
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)}讓我們運行基準測試:
go test ./... -bench=BenchmarkMemoryHeap -benchmem -run=^$ -count=10 > head.txt && benchstat head.txtgo test ./... -bench=BenchmarkMemoryStack -benchmem -run=^$ -count=10 > stack.txt && benchstat stack.txt以下是統計數據:
name time/opMemoryHeap-4 75.0ns ± 5%name alloc/opMemoryHeap-4 96.0B ± 0%name allocs/opMemoryHeap-4 1.00 ± 0%------------------name time/opMemoryStack-4 8.93ns ± 4%name alloc/opMemoryStack-4 0.00Bname allocs/opMemoryStack-4 0.00在這里,使用結構體副本比指針快 8 倍。
為了理解原因,讓我們看看追蹤生成的圖表:
第一張圖非常簡單。由于沒有使用堆,因此沒有垃圾收集器,也沒有額外的 goroutine。對于第二張圖,使用指針迫使 go 編譯器將變量逃逸到堆[1],由此增大了垃圾回收器的壓力。如果我們放大圖表,我們可以看到,垃圾回收器占據了進程的重要部分。
在這張圖中,我們可以看到,垃圾回收器每隔 4ms 必須工作一次。如果我們再次縮放,我們可以詳細了解正在發生的事情:
藍色,粉色和紅色是垃圾收集器的不同階段,而棕色的是與堆上的分配相關(在圖上標有 “runtime.bgsweep”):
清掃是指回收與堆內存中未標記為使用中的值相關聯的內存。當應用程序 Goroutines嘗試在堆內存中分配新值時,會觸發此活動。清掃的延遲被添加到在堆內存中執行分配的成本中,并且與垃圾收集相關的任何延遲沒有關系。
Go 中的垃圾回收:第一部分 - 基礎[2]
即使這個例子有點極端,我們也可以看到,與棧相比,在堆上為變量分配內存是多么消耗資源。在我們的示例中,與在堆上分配內存并共享指針相比,代碼在棧上分配結構體并復制副本要快得多。
如果你不熟悉堆棧或堆,如果你想更多地了解棧或堆的內部細節,你可以在網上找到很多資源,比如 Paul Gribble 的這篇文章[3]。
如果我們使用 GOMAXPROCS = 1 將處理器限制為 1,情況會更糟:
name time/opMemoryHeap 114ns ± 4%name alloc/opMemoryHeap 96.0B ± 0%name allocs/opMemoryHeap 1.00 ± 0%------------------name time/opMemoryStack 8.77ns ± 5%name alloc/opMemoryStack 0.00Bname allocs/opMemoryStack 0.00如果棧上分配的基準數據不變,則堆上的基準從 75ns/op 降低到 114ns/op。
2.方法調用密集型
對于第二個用例,我們將在結構體中添加兩個空方法,稍微調整一下我們的基準測試:
func (s S) stack(s1 S) {}func (s *S) heap(s1 *S) {}在棧上分配的基準測試將創建一個結構體并通過復制副本傳遞它:
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) } }}堆的基準測試將通過指針傳遞結構體:
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) } }}正如預期的那樣,結果現在大不相同:
name time/opMemoryHeap-4 301μs ± 4%name alloc/opMemoryHeap-4 0.00Bname allocs/opMemoryHeap-4 0.00------------------name time/opMemoryStack-4 595μs ± 2%name alloc/opMemoryStack-4 0.00Bname allocs/opMemoryStack-4 0.00結論
在 go 中使用指針而不是結構體的副本并不總是好事。為了能為你的數據選擇好的語義,我強烈建議您閱讀 Bill Kennedy[4] 撰寫的關于值/指針語義的文章[5]。它將為你提供更好的視角來決定使用自定義類型或內置類型時的策略。
此外,內存使用情況分析肯定會幫助你弄清楚你的內存分配和堆上發生了什么。
總結
以上是生活随笔為你收集整理的new 结构体指针_Go:我应该用指针替代结构体的副本吗?的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: python表示当前对象_对象操作
- 下一篇: 一个公网ip多少钱_一个丛书书号多少钱