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

[インデックス 14447] ファイルの概要

このコミットは、Go言語の標準ライブラリである net/http および net/http/httputil パッケージにおけるテストの修正に関するものです。具体的には、TestChunkReaderAllocs というメモリ割り当てテストが、GOMAXPROCS の値が1より大きい場合に失敗する問題を解決しています。

コミット

commit 7bce6f9386153d77ad1293ae1e39cfa214d9d02c
Author: Shenghou Ma <minux.ma@gmail.com>
Date:   Wed Nov 21 02:18:34 2012 +0800

    net/http, net/http/httputil: fix TestChunkReaderAllocs failure when GOMAXPROCS > 1
    
    R=fullung, bradfitz, dave
    CC=golang-dev
    https://golang.org/cl/6846081

GitHub上でのコミットページへのリンク

https://github.com/golang/go/commit/7bce6f9386153d77ad1293ae1e39cfa214d9d02c

元コミット内容

net/http, net/http/httputil: fix TestChunkReaderAllocs failure when GOMAXPROCS > 1

このコミットメッセージは、net/http および net/http/httputil パッケージ内の TestChunkReaderAllocs というテストが、GOMAXPROCS が1より大きい場合に失敗するという問題を修正したことを示しています。

変更の背景

Go言語のテストにおいて、メモリ割り当て(アロケーション)のテストは非常に重要です。これは、プログラムのパフォーマンスに直接影響するため、不要なメモリ割り当てを特定し、削減することが目的です。TestChunkReaderAllocs は、HTTPのチャンク転送エンコーディングを処理する際のメモリ割り当てをテストするものです。

問題の背景には、Goランタイムの GOMAXPROCS 環境変数があります。GOMAXPROCS は、Goプログラムが同時に実行できるOSスレッドの最大数を制御します。Go 1.5より前のバージョンでは、GOMAXPROCS のデフォルト値は1でした。つまり、Goプログラムはデフォルトで単一のOSスレッド上で実行されていました。しかし、GOMAXPROCS を1より大きい値に設定すると、Goランタイムは複数のOSスレッドを利用してゴルーチンを並行して実行するようになります。

メモリ割り当てのテストは、通常、非常に厳密な条件の下で行われます。複数のOSスレッドが同時に動作し、メモリ割り当てが並行して行われる環境では、テストの再現性が損なわれたり、予期せぬメモリ割り当てが発生したりする可能性があります。特に、テスト対象のコードが並行処理を考慮していない場合や、テスト自体が並行処理の影響を受けやすい設計になっている場合に、このような問題が発生しやすくなります。

このコミットは、GOMAXPROCS が1より大きい環境で TestChunkReaderAllocs が失敗するという具体的な問題に対処しています。これは、テストが単一スレッド環境を前提としていたか、あるいは複数スレッド環境でのメモリ割り当ての挙動がテストの期待値と異なっていたためと考えられます。

前提知識の解説

GOMAXPROCS

GOMAXPROCS は、Goランタイムが同時に実行できるOSスレッドの最大数を設定する環境変数、または runtime.GOMAXPROCS 関数によって設定される値です。

  • 目的: Goスケジューラがゴルーチンを並行して実行するために使用できるOSスレッドの数を制限します。これにより、Goプログラムの並列性を制御します。
  • デフォルト値: Go 1.5以降、GOMAXPROCS のデフォルト値は論理CPUコア数に設定されます。Go 1.5より前は、デフォルトは1でした。
  • ゴルーチンとOSスレッド: GOMAXPROCS はゴルーチンの数自体を制限するものではありません。ゴルーチンは軽量であり、Goランタイムはそれらを GOMAXPROCS で制限された利用可能なOSスレッドにスケジュールします。
  • パフォーマンスへの影響: ほとんどのアプリケーションでは、デフォルト設定(CPUコア数)が最適です。GOMAXPROCS をCPUコア数より高く設定しても、パフォーマンスが向上しないばかりか、コンテキストスイッチのオーバーヘッドが増加する可能性があります。

メモリ割り当てテストのような厳密なテストでは、GOMAXPROCS の値がテスト結果に影響を与えることがあります。特に、テストが特定のメモリ割り当てパターンやタイミングに依存している場合、並行処理によってそのパターンが崩れる可能性があります。

defer キーワード

Go言語の defer キーワードは、関数がリターンする直前に実行される関数呼び出しをスケジュールするために使用されます。これは、ファイルのクローズ、ミューテックスのアンロック、リソースの解放など、クリーンアップ操作を確実に行うために非常に便利です。

  • 遅延実行: defer された関数は、それを囲む関数が実行を完了する直前まで実行が延期されます。
  • 引数の評価: defer ステートメントが評価される時点で、defer された関数の引数はすぐに評価されますが、関数呼び出し自体は後で実行されます。
  • LIFO順序: 複数の defer ステートメントがある場合、それらはLIFO(後入れ先出し)順序で実行されます。つまり、最後に defer された関数が最初に実行されます。

このコミットでは、defer runtime.GOMAXPROCS(runtime.GOMAXPROCS(1)) という形で defer が使用されています。これは、テストの実行中に一時的に GOMAXPROCS を1に設定し、テストが終了する際に元の GOMAXPROCS の値に戻すという、非常に一般的なイディオムです。これにより、テストの実行環境を制御し、テストの独立性と再現性を高めることができます。

HTTPチャンク転送エンコーディング (HTTP Chunked Transfer Encoding)

HTTPチャンク転送エンコーディングは、HTTP/1.1で導入されたデータ転送メカニズムです。これは、サーバーがレスポンスの全長を事前に知ることなく、データを一連の「チャンク」としてクライアントに送信することを可能にします。

  • 仕組み: データストリームは複数のチャンクに分割されます。各チャンクは、そのサイズ(16進数)とデータ本体、そして改行で構成されます。転送の終了は、サイズが0のチャンクによって示されます。
  • Transfer-Encoding ヘッダー: HTTPヘッダーに Transfer-Encoding: chunked が含まれている場合、チャンクエンコーディングが使用されていることを示します。
  • 利点:
    • 動的なコンテンツ: サーバーがレスポンスのサイズを事前に決定できない場合に特に有用です。
    • 応答性の向上: クライアントはデータが到着し次第処理を開始できるため、ユーザー体験が向上します。
    • サーバーメモリ使用量の削減: サーバーはレスポンス全体をバッファリングする必要がないため、リソースを最適化できます。
    • ストリーミング: ライブストリームなどのリアルタイムデータ転送を可能にします。

net/http および net/http/httputil パッケージは、このチャンク転送エンコーディングの処理をGoアプリケーションで実現するための機能を提供しています。TestChunkReaderAllocs は、これらの機能がメモリを効率的に使用しているかを検証するテストです。

技術的詳細

このコミットの技術的な核心は、メモリ割り当てテストの信頼性を確保するために、テスト実行時のGoランタイムの並行性設定を一時的に変更することにあります。

TestChunkReaderAllocs は、HTTPチャンクリーダーがデータを読み取る際に発生するメモリ割り当ての量を測定することを目的としています。メモリ割り当ての測定は、非常に繊細な操作であり、Goランタイムの内部的な動作、特にガベージコレクションやスケジューリングの挙動に大きく影響されます。

GOMAXPROCS が1より大きい場合、Goランタイムは複数のOSスレッドを使用してゴルーチンを並行して実行します。この並行性は、メモリ割り当てのタイミングや順序に影響を与える可能性があります。例えば、複数のゴルーチンが同時にメモリを要求したり解放したりすることで、ガベージコレクタの動作が変化し、結果として testing.AllocsPerRun のようなメモリ割り当て測定関数の結果が不安定になることがあります。

具体的には、testing.AllocsPerRun は、指定された関数が実行される間に発生するメモリ割り当ての回数を測定します。この測定は、単一スレッド環境で最も安定して行われます。複数スレッド環境では、以下のような要因がテスト結果の変動を引き起こす可能性があります。

  1. ガベージコレクションのタイミング: 複数スレッドが同時に動作することで、ガベージコレクタがいつ、どのように実行されるかのタイミングが予測しにくくなります。これにより、テスト実行中に発生する一時的なオブジェクトの割り当てと解放のパターンが変化し、測定される割り当て回数に影響を与える可能性があります。
  2. メモリキャッシュの競合: 複数スレッドが同じメモリ領域にアクセスしようとすると、CPUのキャッシュラインの競合が発生し、メモリ割り当てのパフォーマンスに影響を与えることがあります。
  3. スケジューリングの非決定性: Goスケジューラは、利用可能なOSスレッドにゴルーチンをどのように割り当てるかを決定します。GOMAXPROCS > 1 の場合、このスケジューリングは非決定論的になり、テストの実行ごとに異なるゴルーチンの実行順序やタイミングが生じる可能性があります。これにより、メモリ割り当てのパターンも変動し、テストが不安定になる原因となります。

この問題を解決するために、コミットでは TestChunkReaderAllocs の実行中に一時的に GOMAXPROCS を1に設定しています。これにより、テストは単一のOSスレッド上で実行され、メモリ割り当ての測定がより予測可能で安定した環境で行われるようになります。

defer runtime.GOMAXPROCS(runtime.GOMAXPROCS(1)) というイディオムは、以下のステップで動作します。

  1. runtime.GOMAXPROCS(1) が呼び出されます。この関数は、現在の GOMAXPROCS の値を1に設定し、変更前の GOMAXPROCS の値を返します
  2. この返された値が、defer ステートメントの引数として runtime.GOMAXPROCS に渡されます。
  3. defer ステートメントは、この runtime.GOMAXPROCS(元の値) の呼び出しを、現在の関数 (TestChunkReaderAllocs) が終了する直前に実行するようにスケジュールします。

これにより、テストの実行中は GOMAXPROCS が1に設定され、テストが完了すると元の GOMAXPROCS の値に自動的に戻されるため、他のテストやプログラムの動作に影響を与えることなく、テストの独立性が保たれます。

コアとなるコードの変更箇所

このコミットでは、以下の2つのファイルにそれぞれ2行の変更が加えられています。

  1. src/pkg/net/http/chunked_test.go
  2. src/pkg/net/http/httputil/chunked_test.go

それぞれのファイルの TestChunkReaderAllocs 関数に、以下の2行が追加されています。

--- a/src/pkg/net/http/chunked_test.go
+++ b/src/pkg/net/http/chunked_test.go
@@ -42,6 +42,8 @@ func TestChunk(t *testing.T) {
 }
 
 func TestChunkReaderAllocs(t *testing.T) {
+	// temporarily set GOMAXPROCS to 1 as we are testing memory allocations
+	defer runtime.GOMAXPROCS(runtime.GOMAXPROCS(1))
 	var buf bytes.Buffer
 	w := newChunkedWriter(&buf)
 	a, b, c := []byte("aaaaaa"), []byte("bbbbbbbbbbbb"), []byte("cccccccccccccccccccccccc")
diff --git a/src/pkg/net/http/httputil/chunked_test.go b/src/pkg/net/http/httputil/chunked_test.go
index 22c1bb7548..a06bffad5b 100644
--- a/src/pkg/net/http/httputil/chunked_test.go
+++ b/src/pkg/net/http/httputil/chunked_test.go
@@ -44,6 +44,8 @@ func TestChunk(t *testing.T) {
 }
 
 func TestChunkReaderAllocs(t *testing.T) {
+	// temporarily set GOMAXPROCS to 1 as we are testing memory allocations
+	defer runtime.GOMAXPROCS(runtime.GOMAXPROCS(1))
 	var buf bytes.Buffer
 	w := NewChunkedWriter(&buf)
 	a, b, c := []byte("aaaaaa"), []byte("bbbbbbbbbbbb"), []byte("cccccccccccccccccccccccc")

コアとなるコードの解説

追加された2行は、どちらのファイルでも同じ目的を果たしています。

// temporarily set GOMAXPROCS to 1 as we are testing memory allocations
defer runtime.GOMAXPROCS(runtime.GOMAXPROCS(1))
  1. // temporarily set GOMAXPROCS to 1 as we are testing memory allocations これはコメントであり、このコードが何をしているのか、そしてその理由を明確に説明しています。「メモリ割り当てをテストしているため、一時的に GOMAXPROCS を1に設定する」という意図が示されています。これは、前述の通り、メモリ割り当てテストが並行処理の影響を受けやすく、安定した結果を得るために単一スレッド環境が必要であることを示唆しています。

  2. defer runtime.GOMAXPROCS(runtime.GOMAXPROCS(1)) この行が実際の修正の核心です。

    • 内側の runtime.GOMAXPROCS(1): この関数呼び出しは、現在の GOMAXPROCS の値を1に設定します。同時に、この関数は呼び出し前の GOMAXPROCS の値を返します。
    • 外側の defer runtime.GOMAXPROCS(...): defer キーワードは、この runtime.GOMAXPROCS の呼び出しを、TestChunkReaderAllocs 関数が終了する直前に実行するようにスケジュールします。引数には、内側の呼び出しで返された「元の GOMAXPROCS の値」が渡されます。

このメカニズムにより、TestChunkReaderAllocs 関数が実行されている間は GOMAXPROCS が1に設定され、テストが完了して関数がリターンする際には、GOMAXPROCS が元の値に自動的に復元されます。これにより、この特定のテストの実行環境を隔離し、メモリ割り当てテストの信頼性と再現性を向上させています。

この修正は、Goのテストフレームワークにおける一般的なプラクティスを示しており、特定のテストが外部環境(この場合は GOMAXPROCS の設定)に依存して不安定になる問題を解決するための効果的な方法です。

関連リンク

参考にした情報源リンク

  • Go GOMAXPROCS の説明に関するWeb検索結果
  • Go defer キーワードに関するWeb検索結果
  • HTTP チャンク転送エンコーディングに関するWeb検索結果
  • Go メモリ割り当てテストの並行性問題に関するWeb検索結果
  • Go の defer ステートメントに関する公式ドキュメントやチュートリアル
  • Go の GOMAXPROCS の歴史と影響に関する記事
  • Go のメモリプロファイリングとテストに関する記事