[インデックス 16318] ファイルの概要
このコミットは、Go言語のランタイムにおけるメモリ確保(malloc)のパフォーマンスを測定するための新しいベンチマークを追加するものです。特に、アロケーションサイズ(8バイトと16バイト)と型情報(type info)の有無がメモリ確保のパスに与える影響を評価することを目的としています。
コミット
commit 915784e11a58189524c9797ad5e1c1fc43eb632b
Author: Dmitriy Vyukov <dvyukov@google.com>
Date: Wed May 15 21:22:32 2013 +0400
runtime: add simple malloc benchmarks
Allocs of size 16 can bypass atomic set of the allocated bit, while allocs of size 8 can not.
Allocs with and w/o type info hit different paths inside of malloc.
Current results on linux/amd64:
BenchmarkMalloc8 50000000 43.6 ns/op
BenchmarkMalloc16 50000000 46.7 ns/op
BenchmarkMallocTypeInfo8 50000000 61.3 ns/op
BenchmarkMallocTypeInfo16 50000000 63.5 ns/op
R=golang-dev, remyoudompheng, minux.ma, bradfitz, iant
CC=golang-dev
https://golang.org/cl/9090045
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/915784e11a58189524c9797ad5e1c1fc43eb632b
元コミット内容
Goランタイムにシンプルなメモリ確保ベンチマークを追加。 16バイトのアロケーションは、アロケートされたビットのアトミックな設定をバイパスできるが、8バイトのアロケーションはできない。 型情報を持つアロケーションと持たないアロケーションは、malloc内部で異なるパスを通る。 linux/amd64での現在の結果: BenchmarkMalloc8 50000000 43.6 ns/op BenchmarkMalloc16 50000000 46.7 ns/op BenchmarkMallocTypeInfo8 50000000 61.3 ns/op BenchmarkMallocTypeInfo16 50000000 63.5 ns/op
変更の背景
Go言語のランタイムは、効率的なメモリ管理とガベージコレクション(GC)のために高度に最適化されています。メモリ確保のパフォーマンスは、Goアプリケーション全体のパフォーマンスに直接影響を与えるため、その挙動を詳細に理解し、最適化することは非常に重要です。
このコミットの背景には、特定のメモリ確保シナリオにおけるパフォーマンス特性を明らかにするという目的があります。具体的には、以下の点が挙げられます。
- アロケーションサイズによる挙動の違いの検証: Goのメモリ管理システムは、アロケーションサイズに応じて異なるパスを使用することがあります。特に、8バイトと16バイトという特定のサイズが言及されているのは、Goの内部的なメモリ管理単位(例えば、
mspan
のサイズクラスやアライメント要件)に関連している可能性が高いです。コミットメッセージにある「16バイトのアロケーションは、アロケートされたビットのアトミックな設定をバイパスできるが、8バイトのアロケーションはできない」という記述は、これらのサイズがランタイムの内部的な最適化にどのように影響するかを調査するためのものです。 - 型情報の有無による挙動の違いの検証: Goのガベージコレクタは、オブジェクトの型情報を使用して、ポインタが含まれているかどうかを判断し、到達可能なオブジェクトを追跡します。型情報を持つオブジェクトと持たないオブジェクトでは、メモリ確保時およびGC時の処理パスが異なる場合があります。このベンチマークは、型情報の有無がメモリ確保のオーバーヘッドにどの程度影響するかを測定することを目的としています。
- パフォーマンス回帰の検出: 新しいベンチマークを追加することで、将来のランタイムの変更がメモリ確保のパフォーマンスに悪影響を与えないかを継続的に監視できるようになります。これは、Goのパフォーマンスを維持・向上させる上で不可欠なプラクティスです。
これらのベンチマークは、Goランタイム開発者がメモリ管理のボトルネックを特定し、さらなる最適化の機会を見つけるための貴重なデータを提供します。
前提知識の解説
このコミットを理解するためには、Go言語のメモリ管理とガベージコレクションに関する基本的な知識が必要です。
Goのメモリ管理の概要
Goのランタイムは、独自のメモリ管理システムを持っています。これは、OSから大きなメモリブロックを確保し、それをGoのヒープとして管理します。ヒープは、オブジェクトのサイズに応じて異なる方法で管理されます。
- mspan: Goのメモリ管理の基本的な単位は
mspan
です。これは、特定のサイズのオブジェクトを格納するために予約された、連続したメモリページのセットです。Goは、様々なサイズのmspan
を事前に定義しており、小さなオブジェクト(small objects)はこれらのmspan
から効率的に割り当てられます。 - mcache: 各ゴルーチン(論理的なCPU)は、ローカルなメモリキャッシュである
mcache
を持っています。これにより、ロックなしで小さなオブジェクトを高速に割り当てることができます。mcache
が枯渇すると、mcentral
から新しいmspan
を取得します。 - mcentral:
mcentral
は、特定のサイズのmspan
のリストを管理します。これは、複数のmcache
間で共有され、mcache
がmspan
を要求したり、使い終わったmspan
を返したりする際に使用されます。mcentral
へのアクセスはロックによって保護されます。 - mheap:
mheap
は、Goのヒープ全体を管理する最上位のコンポーネントです。mheap
は、OSからメモリを要求し、それをmspan
に分割してmcentral
に提供します。大きなオブジェクト(large objects)は、mheap
から直接割り当てられます。
アロケーションサイズとアライメント
Goのメモリ管理では、アロケーションサイズが非常に重要です。特に、CPUのワードサイズ(通常は8バイト)やキャッシュラインサイズ(通常は64バイト)に合わせたアライメントがパフォーマンスに影響を与えます。
- 8バイトと16バイト: 多くのシステムでは、ポインタや
int64
のようなデータ型は8バイトです。16バイトは、2つのポインタやcomplex64
のようなデータ型、あるいは特定の構造体のアライメント要件を満たすために重要なサイズです。コミットメッセージで言及されている「アロケートされたビットのアトミックな設定をバイパスできる」という点は、特定のサイズのアロケーションが、メモリブロックのステータスを更新する際のアトミック操作を回避できるような最適化パスを通ることを示唆しています。これは、ロックの競合を減らし、パフォーマンスを向上させるための重要な最適化です。
型情報(Type Information)とガベージコレクション
Goのガベージコレクタは、正確なGC(Precise GC)です。これは、ヒープ上のすべてのポインタを正確に識別し、到達可能なオブジェクトのみを保持することを意味します。
- 型情報の役割: Goのコンパイラは、各型に関するメタデータ(型情報)を生成します。このメタデータには、その型がポインタを含むかどうか、含まれる場合はどのオフセットにポインタがあるか、といった情報が含まれます。ガベージコレクタは、この型情報を使用して、ヒープ上のオブジェクトをスキャンし、ポインタをたどって到達可能なオブジェクトをマークします。
- 型情報の有無によるアロケーションパスの違い: 型情報を持つオブジェクト(例えば、構造体やスライス、マップなど)を割り当てる場合、ランタイムはGCがそのオブジェクトを正しくスキャンできるように、追加のメタデータを関連付ける必要があります。一方、型情報を持たないオブジェクト(例えば、
[]byte
のような純粋なデータ)を割り当てる場合、この追加の処理は不要です。この違いが、メモリ確保のパフォーマンスに影響を与える可能性があります。
unsafe.Pointer
unsafe.Pointer
は、Goの型システムをバイパスして、任意の型のポインタを表現できる特殊な型です。これは、C言語のvoid*
に似ています。ベンチマークコードでは、unsafe.Pointer
を使用して、割り当てられたメモリのアドレスをuintptr
に変換し、それをmallocSink
に代入しています。これは、コンパイラが割り当てられたメモリを最適化によって削除しないようにするための一般的なベンチマーク手法です。unsafe
パッケージは、Goの型安全性を損なう可能性があるため、通常は注意して使用されますが、低レベルのランタイムベンチマークでは不可欠なツールです。
技術的詳細
このコミットで追加されたベンチマークは、Goのtesting
パッケージのベンチマーク機能を利用しています。各ベンチマーク関数は、b *testing.B
引数を受け取り、b.N
回ループして操作を実行します。b.N
は、ベンチマーク実行時に自動的に調整され、統計的に有意な結果が得られるように十分な回数実行されます。
ベンチマークの設計
追加されたベンチマークは以下の4種類です。
BenchmarkMalloc8
: 8バイトのオブジェクト(int64
)を割り当てます。new(int64)
は、int64
型のゼロ値へのポインタを返します。int64
はプリミティブ型であり、それ自体はポインタを含まないため、このアロケーションは「型情報なし」のパスに近い挙動を示すと予想されます。BenchmarkMalloc16
: 16バイトのオブジェクト([2]int64
)を割り当てます。new([2]int64)
は、2つのint64
要素を持つ配列へのポインタを返します。これもプリミティブ型の配列であり、ポインタを含まないため、「型情報なし」のパスに近い挙動を示すと予想されます。BenchmarkMallocTypeInfo8
: 8バイトのオブジェクトを割り当てますが、そのオブジェクトはポインタを含む構造体です。具体的には、struct { p [8 / unsafe.Sizeof(uintptr(0))]*int }
という構造体を使用しています。unsafe.Sizeof(uintptr(0))
はポインタのサイズ(通常8バイト)を返すため、8 / unsafe.Sizeof(uintptr(0))
は1となります。つまり、この構造体は[1]*int
、すなわち1つの*int
ポインタを含む構造体となります。これにより、ランタイムはアロケーション時に型情報を考慮し、GCがポインタをスキャンできるように準備する必要があります。BenchmarkMallocTypeInfo16
: 16バイトのオブジェクトを割り当てますが、そのオブジェクトはポインタを含む構造体です。具体的には、struct { p [16 / unsafe.Sizeof(uintptr(0))]*int }
という構造体を使用しています。これは、2つの*int
ポインタを含む構造体となります。同様に、ランタイムはアロケーション時に型情報を考慮する必要があります。
mallocSink
の役割
各ベンチマーク関数では、割り当てられたオブジェクトのポインタをuintptr
に変換し、それをグローバル変数mallocSink
にXOR演算で代入しています。
var x uintptr
for i := 0; i < b.N; i++ {
p := new(int64) // または他の型
x ^= uintptr(unsafe.Pointer(p))
}
mallocSink = x
このmallocSink
の目的は、コンパイラによる最適化を防ぐことです。もし割り当てられたオブジェクトがどこにも使用されない場合、コンパイラは「デッドコードエリミネーション」によってそのアロケーション自体を削除してしまう可能性があります。mallocSink
に値を代入することで、コンパイラはp
が使用されていると判断し、アロケーションが実際に行われることを保証します。XOR演算を使用しているのは、単に代入するだけでなく、複数のポインタの値を累積することで、より確実に最適化を防ぐためです。
測定結果の解釈
コミットメッセージに記載されている結果は、以下の傾向を示しています。
BenchmarkMalloc8
vsBenchmarkMalloc16
: 8バイトのアロケーション(43.6 ns/op)は、16バイトのアロケーション(46.7 ns/op)よりもわずかに高速です。これは、コミットメッセージの「16バイトのアロケーションは、アロケートされたビットのアトミックな設定をバイパスできるが、8バイトのアロケーションはできない」という説明と矛盾するように見えるかもしれません。しかし、これは特定のランタイムの最適化パスや、アライメント、キャッシュの挙動など、様々な要因が絡み合っているため、単純な比較では説明できない複雑な挙動を示している可能性があります。あるいは、この時点での最適化がまだ完全ではなかった可能性も考えられます。- 型情報の有無による影響: 型情報を持つアロケーション(
BenchmarkMallocTypeInfo8
とBenchmarkMallocTypeInfo16
)は、型情報を持たないアロケーション(BenchmarkMalloc8
とBenchmarkMalloc16
)と比較して、明らかに遅いです(約15〜20 ns/opの差)。これは、型情報を処理するための追加のオーバーヘッド(例えば、GCメタデータの更新や、GCスキャンパスの準備)が存在することを示しています。 BenchmarkMallocTypeInfo8
vsBenchmarkMallocTypeInfo16
: 型情報を持つアロケーションの場合も、8バイト(61.3 ns/op)と16バイト(63.5 ns/op)で同様に16バイトの方がわずかに遅い傾向が見られます。
これらの結果は、Goランタイムのメモリ確保が、アロケーションサイズと型情報の有無によって異なるパフォーマンス特性を持つことを明確に示しています。これは、Goアプリケーションのパフォーマンスチューニングや、ランタイムのさらなる最適化のための重要な洞察を提供します。
コアとなるコードの変更箇所
src/pkg/runtime/malloc_test.go
という新しいファイルが追加されています。
--- /dev/null
+++ b/src/pkg/runtime/malloc_test.go
@@ -0,0 +1,52 @@
+// Copyright 2013 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package runtime_test
+
+import (
+ "testing"
+ "unsafe"
+)
+
+var mallocSink uintptr
+
+func BenchmarkMalloc8(b *testing.B) {
+ var x uintptr
+ for i := 0; i < b.N; i++ {
+ p := new(int64)
+ x ^= uintptr(unsafe.Pointer(p))
+ }
+ mallocSink = x
+}
+
+func BenchmarkMalloc16(b *testing.B) {
+ var x uintptr
+ for i := 0; i < b.N; i++ {
+ p := new([2]int64)
+ x ^= uintptr(unsafe.Pointer(p))
+ }
+ mallocSink = x
+}
+
+func BenchmarkMallocTypeInfo8(b *testing.B) {
+ var x uintptr
+ for i := 0; i < b.N; i++ {
+ p := new(struct {
+ p [8 / unsafe.Sizeof(uintptr(0))]*int
+ })
+ x ^= uintptr(unsafe.Pointer(p))
+ }
+ mallocSink = x
+}
+
+func BenchmarkMallocTypeInfo16(b *testing.B) {
+ var x uintptr
+ for i := 0; i < b.N; i++ {
+ p := new(struct {
+ p [16 / unsafe.Sizeof(uintptr(0))]*int
+ })
+ x ^= uintptr(unsafe.Pointer(p))
+ }
+ mallocSink = x
+}
コアとなるコードの解説
追加されたmalloc_test.go
ファイルは、Goのランタイムパッケージのテストスイートの一部として機能します。package runtime_test
という宣言は、このファイルがruntime
パッケージの外部テストであることを示しており、runtime
パッケージの内部実装に直接アクセスするのではなく、公開されたAPIを通じてテストを行うことを意味します。
import
文
"testing"
: Goの標準テストパッケージ。ベンチマーク機能を提供します。"unsafe"
:unsafe.Pointer
やunsafe.Sizeof
などの機能を提供し、型安全性をバイパスしてメモリを直接操作することを可能にします。ランタイムの低レベルな挙動をテストする際に使用されます。
var mallocSink uintptr
このグローバル変数は、前述の通り、コンパイラによる最適化を防ぎ、割り当てられたメモリが実際に使用されることを保証するために使用されます。
ベンチマーク関数
各ベンチマーク関数は、Benchmark
プレフィックスを持ち、*testing.B
型の引数を受け取ります。
-
BenchmarkMalloc8(b *testing.B)
:p := new(int64)
: 8バイトのint64
型の新しいインスタンスをヒープに割り当てます。x ^= uintptr(unsafe.Pointer(p))
: 割り当てられたオブジェクトのポインタをuintptr
に変換し、x
にXOR演算で累積します。これにより、アロケーションが最適化で削除されるのを防ぎます。
-
BenchmarkMalloc16(b *testing.B)
:p := new([2]int64)
: 16バイトの[2]int64
(2つのint64
要素を持つ配列)型の新しいインスタンスをヒープに割り当てます。
-
BenchmarkMallocTypeInfo8(b *testing.B)
:p := new(struct { p [8 / unsafe.Sizeof(uintptr(0))]*int })
:unsafe.Sizeof(uintptr(0))
: システムのポインタサイズ(通常8バイト)を返します。8 / unsafe.Sizeof(uintptr(0))
: この式は1
に評価されます。- したがって、
new(struct { p [1]*int })
となり、これは1つの*int
ポインタを含む8バイトの構造体をヒープに割り当てます。この構造体はポインタを含むため、ランタイムはGCのために型情報を処理する必要があります。
-
BenchmarkMallocTypeInfo16(b *testing.B)
:p := new(struct { p [16 / unsafe.Sizeof(uintptr(0))]*int })
:16 / unsafe.Sizeof(uintptr(0))
: この式は2
に評価されます。- したがって、
new(struct { p [2]*int })
となり、これは2つの*int
ポインタを含む16バイトの構造体をヒープに割り当てます。同様に、ランタイムはGCのために型情報を処理します。
これらのベンチマークは、Goのメモリ確保の内部的な挙動を詳細に分析するための基盤を提供し、ランタイムのパフォーマンス特性を理解する上で不可欠なツールとなります。
関連リンク
- Go言語の公式ドキュメント: https://golang.org/doc/
- Goのメモリ管理に関するブログ記事やドキュメント(一般的な情報源)
参考にした情報源リンク
- Go言語のソースコード(特に
src/runtime
ディレクトリ) - Goの
testing
パッケージのドキュメント - Goの
unsafe
パッケージのドキュメント - Goのメモリ管理とガベージコレクションに関する技術記事や論文(一般的な知識として)
- コミットメッセージ内のGo CLリンク: https://golang.org/cl/9090045