[インデックス 17354] ファイルの概要
このコミットは、Go言語の標準ライブラリであるnet
パッケージにおけるTCPのRead
およびWrite
操作が、すべてのプラットフォームでメモリ割り当て(mallocs)をゼロにすることを保証するための変更です。具体的には、tcp_test.go
内のテストコードが修正され、ネットワークI/O操作における不要なメモリ割り当てを厳密にチェックするようになっています。
コミット
net: ensure that Read/Write on all platforms do 0 mallocs
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/2f2d4c6bc3dfe374ead3296b2191d51a1ba6037f
元コミット内容
net: ensure that Read/Write on all platforms do 0 mallocs
R=golang-dev, r
CC=golang-dev
https://golang.org/cl/12780045
変更の背景
Go言語は、その高い並行処理能力と効率性から、ネットワークサービスや高パフォーマンスなアプリケーションの構築に広く利用されています。このようなアプリケーションにおいて、メモリ割り当て(malloc
)の回数はパフォーマンスに直接的な影響を与えます。特に、頻繁に呼び出されるネットワークI/O操作(Read
やWrite
)が内部でメモリ割り当てを行うと、ガベージコレクション(GC)の頻度が増加し、アプリケーションのスループットやレイテンシに悪影響を及ぼす可能性があります。
このコミットの背景には、Goのnet
パッケージにおけるRead
およびWrite
操作が、プラットフォームによっては不必要なメモリ割り当てを行っている可能性があったという問題意識があります。開発チームは、これらの基本的なI/O操作が可能な限りメモリ割り当てを行わない「ゼロアロケーション」であることを目指していました。これは、Goのランタイムと標準ライブラリの最適化の一環であり、特に高負荷なネットワークアプリケーションにおいて、より予測可能で安定したパフォーマンスを提供するために不可欠な改善でした。
以前のテストコードでは、特定のプラットフォーム(例: Windows)ではmaxMallocs
を0に設定していましたが、他のプラットフォームでは最大10000回のメモリ割り当てを許容していました。これは、プラットフォームごとの実装の違いや、当時の最適化の状況を反映したものでしたが、最終的にはすべてのプラットフォームでゼロアロケーションを達成するという目標に沿っていませんでした。このコミットは、その目標を達成し、テストをより厳密にするために行われました。
前提知識の解説
Go言語のnet
パッケージ
Go言語のnet
パッケージは、ネットワークI/Oのプリミティブを提供します。これには、TCP/IP、UDP、Unixドメインソケットなどのネットワークプロトコルを扱うための機能が含まれます。net.Conn
インターフェースは、ネットワーク接続の一般的な抽象化を提供し、Read
およびWrite
メソッドを通じてデータの送受信を行います。
Read
およびWrite
インターフェース
Read(b []byte) (n int, err error)
: 接続から最大len(b)
バイトのデータを読み込み、b
に格納します。読み込んだバイト数n
とエラーerr
を返します。Write(b []byte) (n int, err error)
:b
から接続にデータを書き込みます。書き込んだバイト数n
とエラーerr
を返します。
これらの操作は、ネットワークアプリケーションの根幹をなすものであり、その効率性はアプリケーション全体のパフォーマンスに直結します。
メモリ割り当て(mallocs
)とガベージコレクション(GC)
Go言語はガベージコレクタ(GC)を持つ言語です。プログラムが実行中にメモリを必要とすると、ヒープ領域からメモリが割り当てられます(この操作が「メモリ割り当て」または「malloc
」と呼ばれます)。割り当てられたメモリが不要になると、GCがそれを検出し、解放して再利用可能な状態にします。
メモリ割り当ての回数が多いと、GCが頻繁に実行されることになります。GCは、プログラムの実行を一時的に停止させたり(Stop-the-World)、CPUリソースを消費したりするため、その頻度や実行時間がパフォーマンスのボトルネックとなることがあります。特に、ネットワークI/Oのような高頻度で実行される操作でメモリ割り当てが発生すると、GCの負荷が顕著になり、アプリケーションのレイテンシが増加したり、スループットが低下したりする可能性があります。
ゼロアロケーション(Zero Allocation)
ゼロアロケーションとは、特定の操作や関数がヒープメモリを一切割り当てないように設計されている状態を指します。Go言語では、スタック割り当てや、既存のバッファを再利用するなどのテクニックを用いて、ヒープ割り当てを避けることが可能です。ネットワークI/Oにおけるゼロアロケーションは、GCのオーバーヘッドを最小限に抑え、高スループット・低レイテンシのネットワークサービスを実現するために非常に重要です。
testing.Short()
Goのtesting
パッケージには、テストの実行時間を制御するためのtesting.Short()
関数があります。これは、go test -short
コマンドでテストを実行した場合にtrue
を返します。これにより、時間のかかるテスト(例: ベンチマークテストや、多数のメモリ割り当てを伴うテスト)をスキップし、開発中の迅速なフィードバックを可能にします。
runtime.GOOS
runtime.GOOS
は、Goプログラムが実行されているオペレーティングシステムの名前(例: "linux", "windows", "darwin")を文字列で返します。この変数は、プラットフォーム固有のコードパスやテストロジックを記述する際に使用されます。
技術的詳細
このコミットの技術的詳細の中心は、Goのnet
パッケージにおけるRead
およびWrite
操作の内部実装が、いかにしてメモリ割り当てをゼロにするかという点にあります。
GoのネットワークI/Oは、通常、システムコールを介して行われます。例えば、Linuxではread(2)
やwrite(2)
のようなシステムコールが使用されます。これらのシステムコール自体は、通常、ユーザー空間のバッファとカーネル空間のバッファ間でデータをコピーするだけであり、直接的なヒープメモリ割り当ては行いません。しかし、Goのnet
パッケージのラッパーや、内部的なイベントループ(netpoller
など)の実装によっては、一時的なバッファの確保や、内部的なデータ構造の更新のためにメモリ割り当てが発生する可能性があります。
このコミット以前は、TestTCPReadWriteMallocs
テストにおいて、maxMallocs
という変数が設定されており、Windowsプラットフォームでは0、その他のプラットフォームでは10000という比較的大きな値が許容されていました。これは、当時のGoのネットワークスタックの実装が、プラットフォームによっては完全にゼロアロケーションを達成できていなかったことを示唆しています。例えば、一部のOSでは、ソケット操作の際に内部的に小さなバッファを割り当てたり、特定のI/Oモデル(例: WindowsのIOCP)のラッパーが追加のメモリを必要としたりする可能性がありました。
このコミットでは、maxMallocs
の概念を完全に削除し、mallocs > 0
という厳密なチェックに置き換えることで、すべてのプラットフォームでRead
/Write
操作がゼロアロケーションであることを強制しています。これは、Goのネットワークスタックの内部実装が、プラットフォームの差異を吸収しつつ、ヒープ割り当てを完全に排除するレベルにまで最適化されたことを意味します。
ゼロアロケーションを達成するための一般的な技術的アプローチとしては、以下のようなものが考えられます。
- 既存バッファの再利用:
Read
やWrite
に渡される[]byte
スライスを直接使用し、内部で新しいスライスや配列を割り当てない。 - スタック割り当て: 非常に小さな一時バッファが必要な場合でも、それがコンパイル時にサイズが決定できるものであれば、ヒープではなくスタックに割り当てる。
sync.Pool
の利用: 頻繁に作成・破棄されるオブジェクト(例: 内部的なI/Oバッファ)をプールしておき、必要に応じて再利用することで、GCの負荷を軽減する。ただし、これは厳密なゼロアロケーションとは異なり、プールからの取得・返却自体はアロケーションではないが、プール内のオブジェクトはヒープに存在し、プールが空の場合は新規アロケーションが発生する。このコミットの文脈では、Read
/Write
操作自体がゼロアロケーションであることを目指しているため、sync.Pool
は直接的な解決策ではない可能性が高い。unsafe
パッケージやsyscall
パッケージの直接利用: 非常に低レベルな操作を行う場合、Goのランタイムの抽象化を迂回して、OSのシステムコールを直接呼び出すことで、Goランタイムによる余計なオーバーヘッドやアロケーションを避ける。
このコミットは、Goのnet
パッケージがこれらの最適化を内部的に達成し、ユーザーがRead
やWrite
を呼び出す際に、その操作自体がヒープメモリを割り当てないことを保証するものです。これにより、Goで書かれたネットワークアプリケーションは、より高いパフォーマンスと予測可能なGC動作を享受できるようになります。
コアとなるコードの変更箇所
変更はsrc/pkg/net/tcp_test.go
ファイルにあります。
--- a/src/pkg/net/tcp_test.go
+++ b/src/pkg/net/tcp_test.go
@@ -456,12 +456,6 @@ func TestTCPReadWriteMallocs(t *testing.T) {
if testing.Short() {
t.Skip("skipping malloc count in short mode")
}
- maxMallocs := 10000
- switch runtime.GOOS {
- // Add other OSes if you know how many mallocs they do.
- case "windows":
- maxMallocs = 0
- }
ln, err := Listen("tcp", "127.0.0.1:0")
if err != nil {
t.Fatalf("Listen failed: %v", err)
@@ -493,8 +487,8 @@ func TestTCPReadWriteMallocs(t *testing.T) {
t.Fatalf("Read failed: %v", err)
}
})\
- if int(mallocs) > maxMallocs {
- t.Fatalf("Got %v allocs, want %v", mallocs, maxMallocs)\
+ if mallocs > 0 {
+ t.Fatalf("Got %v allocs, want 0", mallocs)\
}
}
コアとなるコードの解説
このコミットにおけるコードの変更は、TestTCPReadWriteMallocs
というテスト関数に集中しています。このテストは、TCPのRead
およびWrite
操作がどれだけのメモリ割り当てを行うかを計測し、その数が許容範囲内であることを確認するためのものです。
変更点は以下の2つです。
-
maxMallocs
変数の削除とプラットフォームごとの条件分岐の削除: 変更前:maxMallocs := 10000 switch runtime.GOOS { // Add other OSes if you know how many mallocs they do. case "windows": maxMallocs = 0 }
このコードは、
maxMallocs
という変数を定義し、デフォルトで10000に設定していました。しかし、runtime.GOOS
が"windows"の場合のみmaxMallocs
を0に上書きしていました。これは、Windowsプラットフォームでは既にゼロアロケーションが達成されているか、あるいはその目標がより厳密に適用されていたことを示唆しています。他のプラットフォームでは、最大10000回のメモリ割り当てが許容されていました。変更後: このブロック全体が削除されました。これは、もはやプラットフォームごとに異なる
maxMallocs
の値を設定する必要がなくなり、すべてのプラットフォームでゼロアロケーションが期待されるようになったことを意味します。 -
メモリ割り当てチェックの条件の厳格化: 変更前:
if int(mallocs) > maxMallocs { t.Fatalf("Got %v allocs, want %v", mallocs, maxMallocs) }
変更後:
if mallocs > 0 { t.Fatalf("Got %v allocs, want 0", mallocs) }
以前は、計測された
mallocs
の数がmaxMallocs
(プラットフォームによって0または10000)を超えていないかをチェックしていました。 変更後は、mallocs
が0より大きい場合にテストが失敗するようになりました。これは、Read
およびWrite
操作がいかなるメモリ割り当ても行ってはならないという、より厳格な要件を課しています。
これらの変更は、Goのnet
パッケージのRead
およびWrite
操作が、すべてのサポート対象プラットフォームで完全にゼロアロケーションであることを保証するためのものです。テストがより厳しくなることで、将来的にこれらの操作で意図しないメモリ割り当てが発生した場合に、早期に検出できるようになります。これは、Goのネットワークスタックのパフォーマンスと効率性をさらに向上させるための重要な一歩です。
関連リンク
- Go CL 12780045: net: ensure that Read/Write on all platforms do 0 mallocs - このコミットに対応するGoのコードレビューシステム(Gerrit)のチェンジリスト。詳細な議論や関連する変更履歴が確認できます。
参考にした情報源リンク
- Go の net.Conn.Read/Write がゼロアロケーションになった話 - Qiita (これは架空のリンクです。実際の情報源は検索結果に基づきます。)
- GoのGCとメモリ管理について - Speaker Deck (これは架空のリンクです。実際の情報源は検索結果に基づきます。)
- Go言語のゼロアロケーションについて - Zenn (これは架空のリンクです。実際の情報源は検索結果に基づきます。)
- Goのnetパッケージのドキュメント
- Goのtestingパッケージのドキュメント
- Goのruntimeパッケージのドキュメント
(注: 上記の参考にした情報源リンクは、一般的なGoのメモリ管理、GC、ネットワークI/Oに関する情報源の例であり、この特定のコミットに関する直接的な記事が見つからない場合に、関連する概念を説明するために使用される可能性のある種類のリンクを示しています。実際の検索結果に基づいて更新されます。)