[インデックス 17057] ファイルの概要
このコミットは、Go言語の標準ライブラリ net
パッケージ内の tcp_test.go
ファイルに対する変更です。具体的には、TCP接続における Read
および Write
操作がヒープアロケーションを発生させないことを検証するための新しいベンチマークテストが追加されています。
コミット
commit 905f29655230cac74f0b91bd1f1de112451e61f3
Author: Dmitriy Vyukov <dvyukov@google.com>
Date: Tue Aug 6 21:29:35 2013 +0400
net: test that Read/Write do 0 allocations
It turned out that change 12413043 did not break
any builders. So let's lock this in.
R=golang-dev, rsc
CC=golang-dev
https://golang.org/cl/12545043
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/905f29655230cac74f0b91bd1f1de112451e61f3
元コミット内容
net: test that Read/Write do 0 allocations
It turned out that change 12413043 did not break
any builders. So let's lock this in.
このコミットメッセージは、net
パッケージの Read
/Write
操作がヒープアロケーションを発生させないことをテストするための変更であることを示しています。また、以前の変更 (チェンジリスト 12413043) がビルドを壊さなかったため、この最適化を確定させる意図があることが述べられています。
変更の背景
Go言語のランタイムおよび標準ライブラリでは、パフォーマンスの最適化、特にヒープアロケーションの削減が重要な目標の一つです。ヒープアロケーションはガベージコレクションの負荷を増加させ、プログラムの実行速度に悪影響を与える可能性があります。ネットワークI/Oのような頻繁に実行される操作において、アロケーションが発生すると、その影響は顕著になります。
このコミットの背景には、net
パッケージの Read
および Write
メソッドが、内部的にバッファを再利用したり、スタック上に小さなバッファを確保したりすることで、ヒープアロケーションを避けるように設計されているという前提があります。以前の変更 (チェンジリスト 12413043) でこの最適化が導入された可能性がありますが、それが実際にアロケーションフリーであることを保証し、将来のリグレッションを防ぐためには、専用のベンチマークテストが必要とされました。
特に、net/http
や net/rpc
のような高レベルのネットワークライブラリでは、同じ接続に対して並行して読み書きを行うパターンが頻繁に利用されます。このようなシナリオで Read
/Write
がアロケーションを発生させると、アプリケーション全体のパフォーマンスに大きな影響を与えるため、アロケーションフリーであることの保証は非常に重要です。
前提知識の解説
-
ヒープアロケーションとガベージコレクション (GC):
- ヒープアロケーション: プログラムが実行時に動的にメモリを確保する際に、ヒープ領域からメモリを割り当てることです。Goでは
make
やnew
、あるいは構造体のリテラル初期化などでヒープアロケーションが発生します。 - ガベージコレクション (GC): ヒープに割り当てられたメモリのうち、もはや参照されなくなった(不要になった)メモリを自動的に解放するプロセスです。GCはプログラムの実行を一時停止させることがあり(ストップ・ザ・ワールド)、これがパフォーマンスのボトルネックになることがあります。アロケーションの頻度が高いほど、GCの実行頻度も高くなり、パフォーマンスへの影響が大きくなります。
- アロケーションフリー: 特定の操作がヒープアロケーションを一切発生させないことを指します。これにより、GCのオーバーヘッドを完全に回避し、パフォーマンスを向上させることができます。
- ヒープアロケーション: プログラムが実行時に動的にメモリを確保する際に、ヒープ領域からメモリを割り当てることです。Goでは
-
Goのベンチマークテスト:
- Goには標準でベンチマークテストのフレームワークが組み込まれています。
testing
パッケージを使用し、BenchmarkXxx
という形式の関数を定義することでベンチマークを実行できます。 *testing.B
型の引数b
を受け取り、b.N
回の操作を繰り返すように実装します。b.StopTimer()
とb.StartTimer()
を使用して、セットアップやクリーンアップの時間を計測から除外できます。go test -bench=. -benchmem
コマンドでベンチマークを実行すると、実行時間だけでなく、操作あたりのアロケーション数 (allocs/op
) とアロケーションされたバイト数 (bytes/op
) も計測できます。このコミットの目的は、これらの値が0であることを確認することです。
- Goには標準でベンチマークテストのフレームワークが組み込まれています。
-
runtime.GOMAXPROCS
:- Goのランタイムが同時に実行できるOSスレッドの最大数を制御する環境変数、または関数です。デフォルトではCPUのコア数に設定されます。この値は、並行処理のベンチマークにおいて、同時に実行されるゴルーチンの数を調整するために使用されることがあります。
-
TCPソケットプログラミングの基本:
Listen
: サーバー側で特定のネットワークアドレスとポートで接続を待ち受けるために使用されます。Accept
:Listen
で待ち受けているソケットに対して、クライアントからの接続要求を受け入れ、新しい接続を表すソケットを返します。Dial
: クライアント側で指定されたネットワークアドレスとポートに接続を確立するために使用されます。Read
: 接続からデータを読み取ります。Write
: 接続にデータを書き込みます。Conn
インターフェース:net
パッケージで定義されている、ネットワーク接続の一般的なインターフェースで、Read
、Write
、Close
などのメソッドを持ちます。
技術的詳細
このコミットは、src/pkg/net/tcp_test.go
に BenchmarkTCP4ConcurrentReadWrite
と BenchmarkTCP6ConcurrentReadWrite
という2つの新しいベンチマーク関数を追加しています。これらの関数は、内部的に benchmarkTCPConcurrentReadWrite
を呼び出します。
benchmarkTCPConcurrentReadWrite
ベンチマークの主な目的は、TCP接続における並行な Read
および Write
操作がヒープアロケーションを発生させないことを検証することです。
ベンチマークの設計は以下の特徴を持ちます。
- 並行性の強調:
runtime.GOMAXPROCS(0)
を取得し、その値P
に基づいてP
個のクライアント/サーバーペアを作成します。各ペアは、クライアントの読み書きゴルーチンとサーバーの読み書きゴルーチンをそれぞれ起動し、合計で4 * P
個のゴルーチンが並行して動作します。これは、net/http
やnet/rpc
のような実際のアプリケーションでよく見られる、同じ接続に対する並行な読み書きパターンをシミュレートしています。 - アロケーション計測の準備:
b.StopTimer()
を呼び出して、ベンチマークのセットアップ(接続の確立など)にかかる時間を計測から除外します。テスト対象の操作が開始される直前にb.StartTimer()
が呼び出されます。 - データフロー:
- クライアントの書き込みゴルーチンは、1バイトのデータを生成し、それをTCP接続に書き込みます。
- サーバーの読み込みゴルーチンは、そのデータをTCP接続から読み込み、
pipe
というチャネルを介してサーバーの書き込みゴルーチンに渡します。 - サーバーの書き込みゴルーチンは、
pipe
からデータを受け取り、簡単な計算(v *= v
)を行った後、そのデータをTCP接続に書き込みます。 - クライアントの読み込みゴルーチンは、サーバーからの応答データをTCP接続から読み込みます。
- この一連のデータフローは、各ゴルーチンが
N
回(b.N / P
)繰り返します。
- バッファの利用:
Read
およびWrite
操作には、var buf [1]byte
のようにスタック上に確保された固定サイズのバイトスライス (buf[:]
) が使用されています。これにより、これらの操作がヒープアロケーションを発生させないことが期待されます。 sync.WaitGroup
の利用: すべてのゴルーチンが完了するのを待つためにsync.WaitGroup
が使用されています。これにより、ベンチマークがすべてのI/O操作が完了するまで実行されることが保証されます。numConcurrent
の変更: 既存のbenchmarkTCP
関数内でnumConcurrent
の計算がruntime.GOMAXPROCS(-1) * 16
からruntime.GOMAXPROCS(-1) * 2
に変更されています。これは、おそらく新しいベンチマークの導入に伴い、既存のベンチマークの並行度を調整したか、あるいは以前の変更 (12413043) の影響を考慮した調整であると考えられます。
このベンチマークが成功し、allocs/op
と bytes/op
が0と報告されれば、net
パッケージの Read
/Write
操作が指定されたシナリオでアロケーションフリーであることが確認できます。
コアとなるコードの変更箇所
変更は src/pkg/net/tcp_test.go
ファイルに集中しています。
benchmarkTCP
関数のnumConcurrent
の変更:--- a/src/pkg/net/tcp_test.go +++ b/src/pkg/net/tcp_test.go @@ -61,7 +61,7 @@ func BenchmarkTCP6PersistentTimeout(b *testing.B) { func benchmarkTCP(b *testing.B, persistent, timeout bool, laddr string) { const msgLen = 512 conns := b.N - numConcurrent := runtime.GOMAXPROCS(-1) * 16 + numConcurrent := runtime.GOMAXPROCS(-1) * 2 msgs := 1 if persistent { conns = numConcurrent
- 新しいベンチマーク関数の追加:
BenchmarkTCP4ConcurrentReadWrite
,BenchmarkTCP6ConcurrentReadWrite
, およびそれらを実装するbenchmarkTCPConcurrentReadWrite
関数が追加されています。この追加されたコードブロックは非常に長く、コミットログの差分で124行の追加と1行の削除(上記のnumConcurrent
の変更)を示しています。
コアとなるコードの解説
追加された benchmarkTCPConcurrentReadWrite
関数がこのコミットの核心です。
func benchmarkTCPConcurrentReadWrite(b *testing.B, laddr string) {
// The benchmark creates GOMAXPROCS client/server pairs.
// Each pair creates 4 goroutines: client reader/writer and server reader/writer.
// The benchmark stresses concurrent reading and writing to the same connection.
// Such pattern is used in net/http and net/rpc.
b.StopTimer() // ベンチマーク計測を一時停止
P := runtime.GOMAXPROCS(0) // 論理CPUコア数を取得
N := b.N / P // 各ゴルーチンが実行する操作回数
W := 1000 // ダミーの計算負荷(アロケーションとは無関係)
// P個のクライアント/サーバー接続をセットアップ
clients := make([]Conn, P)
servers := make([]Conn, P)
ln, err := Listen("tcp", laddr) // サーバーのリスナーを作成
if err != nil {
b.Fatalf("Listen failed: %v", err)
}
defer ln.Close()
done := make(chan bool)
go func() { // サーバー側で接続を受け入れるゴルーチン
for p := 0; p < P; p++ {
s, err := ln.Accept()
if err != nil {
b.Fatalf("Accept failed: %v", err)
}
servers[p] = s
}
done <- true // すべてのサーバー接続が確立したら通知
}()
for p := 0; p < P; p++ { // クライアント側で接続を確立
c, err := Dial("tcp", ln.Addr().String())
if err != nil {
b.Fatalf("Dial failed: %v", err)
}
clients[p] = c
}
<-done // サーバー接続確立を待つ
b.StartTimer() // ベンチマーク計測を開始
var wg sync.WaitGroup // ゴルーチンの完了を待つためのWaitGroup
wg.Add(4 * P) // 4 * P個のゴルーチンを追加
for p := 0; p < P; p++ { // 各クライアント/サーバーペアに対してゴルーチンを起動
// クライアントの書き込みゴルーチン
go func(c Conn) {
defer wg.Done()
var buf [1]byte // スタック上に1バイトのバッファを確保
for i := 0; i < N; i++ {
v := byte(i)
for w := 0; w < W; w++ { // ダミーの計算
v *= v
}
buf[0] = v
_, err := c.Write(buf[:]) // Write操作(アロケーションフリーを期待)
if err != nil {
b.Fatalf("Write failed: %v", err)
}
}
}(clients[p])
// サーバーの読み込みゴルーチンと書き込みゴルーチン間のパイプ
pipe := make(chan byte, 128) // バッファ付きチャネル
// サーバーの読み込みゴルーチン
go func(s Conn) {
defer wg.Done()
var buf [1]byte // スタック上に1バイトのバッファを確保
for i := 0; i < N; i++ {
_, err := s.Read(buf[:]) // Read操作(アロケーションフリーを期待)
if err != nil {
b.Fatalf("Read failed: %v", err)
}
pipe <- buf[0] // 読み込んだデータをパイプに送信
}
}(servers[p])
// サーバーの書き込みゴルーチン
go func(s Conn) {
defer wg.Done()
var buf [1]byte // スタック上に1バイトのバッファを確保
for i := 0; i < N; i++ {
v := <-pipe // パイプからデータを受信
for w := 0; w < W; w++ { // ダミーの計算
v *= v
}
buf[0] = v
_, err := s.Write(buf[:]) // Write操作(アロケーションフリーを期待)
if err != nil {
b.Fatalf("Write failed: %v", err)
}
}
s.Close() // サーバー接続をクローズ
}(servers[p])
// クライアントの読み込みゴルーチン
go func(c Conn) {
defer wg.Done()
var buf [1]byte // スタック上に1バイトのバッファを確保
for i := 0; i < N; i++ {
_, err := c.Read(buf[:]) // Read操作(アロケーションフリーを期待)
if err != nil {
b.Fatalf("Read failed: %v", err)
}
}
c.Close() // クライアント接続をクローズ
}(clients[p])
}
wg.Wait() // すべてのゴルーチンの完了を待つ
}
このコードの重要な点は、Read
と Write
の呼び出しにおいて、buf[:]
のようにスタック上に確保された小さなバッファを渡していることです。これにより、これらのI/O操作が内部的にヒープアロケーションを発生させないことを検証できます。もし Read
や Write
が内部で新しいバッファをヒープに割り当ててしまうと、このベンチマークは allocs/op > 0
を報告することになります。
W := 1000
のループは、I/O操作の間にわずかなCPU負荷をかけるためのもので、アロケーションとは直接関係ありませんが、実際のアプリケーションのシナリオをより忠実に再現しようとしていると考えられます。
関連リンク
- Go言語の
net
パッケージ: https://pkg.go.dev/net - Go言語の
testing
パッケージ: https://pkg.go.dev/testing - Go言語の
runtime
パッケージ: https://pkg.go.dev/runtime - Go言語の
sync
パッケージ: https://pkg.go.dev/sync
参考にした情報源リンク
- Go言語のベンチマークに関する公式ドキュメントやブログ記事 (一般的な知識として)
- Go言語のガベージコレクションとアロケーションに関する記事 (一般的な知識として)
- Go言語の
net
パッケージのソースコード (一般的な知識として) - Go言語のチェンジリスト (CL) の概念: https://go.dev/doc/contribute#_code_review (CL 12413043, 12545043 の参照元)
- Goのベンチマークでアロケーションを計測する方法:
go test -bench=. -benchmem
コマンドに関する情報。