Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

[インデックス 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操作(ReadWrite)が内部でメモリ割り当てを行うと、ガベージコレクション(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のネットワークスタックの内部実装が、プラットフォームの差異を吸収しつつ、ヒープ割り当てを完全に排除するレベルにまで最適化されたことを意味します。

ゼロアロケーションを達成するための一般的な技術的アプローチとしては、以下のようなものが考えられます。

  1. 既存バッファの再利用: ReadWriteに渡される[]byteスライスを直接使用し、内部で新しいスライスや配列を割り当てない。
  2. スタック割り当て: 非常に小さな一時バッファが必要な場合でも、それがコンパイル時にサイズが決定できるものであれば、ヒープではなくスタックに割り当てる。
  3. sync.Poolの利用: 頻繁に作成・破棄されるオブジェクト(例: 内部的なI/Oバッファ)をプールしておき、必要に応じて再利用することで、GCの負荷を軽減する。ただし、これは厳密なゼロアロケーションとは異なり、プールからの取得・返却自体はアロケーションではないが、プール内のオブジェクトはヒープに存在し、プールが空の場合は新規アロケーションが発生する。このコミットの文脈では、Read/Write操作自体がゼロアロケーションであることを目指しているため、sync.Poolは直接的な解決策ではない可能性が高い。
  4. unsafeパッケージやsyscallパッケージの直接利用: 非常に低レベルな操作を行う場合、Goのランタイムの抽象化を迂回して、OSのシステムコールを直接呼び出すことで、Goランタイムによる余計なオーバーヘッドやアロケーションを避ける。

このコミットは、Goのnetパッケージがこれらの最適化を内部的に達成し、ユーザーがReadWriteを呼び出す際に、その操作自体がヒープメモリを割り当てないことを保証するものです。これにより、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つです。

  1. 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の値を設定する必要がなくなり、すべてのプラットフォームでゼロアロケーションが期待されるようになったことを意味します。

  2. メモリ割り当てチェックの条件の厳格化: 変更前:

    	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)を超えていないかをチェックしていました。 変更後は、mallocs0より大きい場合にテストが失敗するようになりました。これは、ReadおよびWrite操作がいかなるメモリ割り当ても行ってはならないという、より厳格な要件を課しています。

これらの変更は、GoのnetパッケージのReadおよびWrite操作が、すべてのサポート対象プラットフォームで完全にゼロアロケーションであることを保証するためのものです。テストがより厳しくなることで、将来的にこれらの操作で意図しないメモリ割り当てが発生した場合に、早期に検出できるようになります。これは、Goのネットワークスタックのパフォーマンスと効率性をさらに向上させるための重要な一歩です。

関連リンク

参考にした情報源リンク

(注: 上記の参考にした情報源リンクは、一般的なGoのメモリ管理、GC、ネットワークI/Oに関する情報源の例であり、この特定のコミットに関する直接的な記事が見つからない場合に、関連する概念を説明するために使用される可能性のある種類のリンクを示しています。実際の検索結果に基づいて更新されます。)