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

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

このコミットは、Go言語の標準ライブラリ bytes パッケージ内の buffer_test.go から、巨大なバッファをテストするTestHuge関数を削除するものです。このテストは、特に32ビット環境において、過剰なメモリ消費により信頼性に欠け、問題を引き起こすことが判明したため削除されました。

コミット

commit 87079cc14c98cb82d98f3e564fe5e89cbd7d8ff6
Author: Rob Pike <r@golang.org>
Date:   Sun Jan 22 09:25:47 2012 -0800

    bytes: delete the test for huge buffers
    It takes too much memory to be reliable and causes
    trouble on 32-bit machines.
    Sigh.
    
    Fixes #2756.
    
    R=golang-dev, bradfitz
    CC=golang-dev
    https://golang.org/cl/5567043

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

https://github.com/golang/go/commit/87079cc14c98cb82d98f3e564fe5e89cbd7d8ff6

元コミット内容

bytes: delete the test for huge buffers It takes too much memory to be reliable and causes trouble on 32-bit machines. Sigh.

Fixes #2756.

R=golang-dev, bradfitz CC=golang-dev https://golang.org/cl/5567043

変更の背景

このコミットの背景には、Go言語の標準ライブラリであるbytesパッケージのテストスイートにおける、メモリ消費の問題があります。具体的には、bytes.Bufferが非常に大きなデータを扱う際の挙動を検証するために設計されたTestHugeというテスト関数が、その性質上、大量のメモリを消費していました。

このテストは、特に32ビットシステム上で実行された際に問題を引き起こしました。32ビットシステムでは、プロセスが利用できる仮想メモリ空間が4GBに制限されており、そのうちユーザー空間が利用できるのは通常2GBまたは3GBです。TestHuge関数は、500MBのバイトスライスを1000回bytes.Bufferに書き込むという処理を行っており、これは理論上500GBものデータをバッファに格納しようとします。これは32ビットシステムはもちろん、64ビットシステムでも物理メモリをはるかに超える量であり、テストの実行が不安定になったり、システム全体のパフォーマンスに悪影響を与えたり、最悪の場合、テストがクラッシュする原因となっていました。

テストの目的はコードの正確性を保証することですが、そのテスト自体が環境依存の不安定性を持つことは望ましくありません。特に、メモリ制約の厳しい環境(例えば、CI/CDパイプラインの共有リソースや、古いハードウェアでの開発環境)でテストが失敗すると、開発者の生産性を低下させ、誤ったアラートを引き起こす可能性があります。そのため、このテストは信頼性に欠けると判断され、削除されることになりました。コミットメッセージにある「Sigh.」という表現は、理想的なテストケースではあったものの、実用上の制約により削除せざるを得なかった開発者の無念さを表していると推測されます。

前提知識の解説

bytes.Buffer

bytes.Bufferは、Go言語の標準ライブラリbytesパッケージで提供される、可変長のバイトバッファです。これは、バイトスライス([]byte)を効率的に操作するための型であり、io.Readerおよびio.Writerインターフェースを実装しています。これにより、ファイルやネットワーク接続など、他のI/O操作とシームレスに連携できます。

主な特徴は以下の通りです。

  • 動的なサイズ変更: 内部的にバイトスライスを保持し、必要に応じて自動的に拡張されます。これにより、事前に正確なサイズを知る必要なくデータを書き込むことができます。
  • 効率的な書き込み: WriteメソッドやWriteStringメソッドを通じてデータを追加できます。内部的には、新しいデータが追加される際に、必要に応じてより大きな容量を持つ新しいスライスが割り当てられ、既存のデータがコピーされます。
  • 効率的な読み込み: ReadReadByteReadRuneなどのメソッドでデータを読み込むことができます。読み込まれたデータはバッファから消費されます。
  • メモリ管理: bytes.Bufferは、内部のスライスが拡張される際に、ある程度の余裕を持ってメモリを確保します。これは、頻繁な再割り当てとデータコピーのオーバーヘッドを避けるためです。しかし、非常に大きなデータを扱う場合や、急激なサイズ増加がある場合には、メモリの再割り当てとコピーが頻繁に発生し、パフォーマンスに影響を与える可能性があります。

testing.Short()

testing.Short()は、Go言語のtestingパッケージで提供される関数です。これは、go testコマンドに-shortフラグが指定された場合にtrueを返します。このフラグは、テストスイート全体を高速に実行したい場合に利用され、時間のかかるテストやリソースを大量に消費するテストをスキップするために使用されます。

TestHuge関数は、if testing.Short() { return }というガード句を持っていました。これは、開発者がgo test -shortを実行した場合に、このメモリを大量に消費するテストがスキップされるようにするためのものです。しかし、このガード句があっても、-shortフラグなしでテストが実行された場合には、依然としてメモリ問題が発生する可能性がありました。

ErrTooLarge

bytes.Bufferの操作中に、バッファが許容される最大サイズを超えようとした場合に返されるエラーです。Go言語のbytesパッケージでは、Bufferの最大容量はMaxInt(システムが扱える最大の整数値)に制限されています。これは、内部的にスライスを使用しているため、スライスのインデックスがint型で表現できる範囲に収まる必要があるためです。

TestHuge関数は、このErrTooLargeが適切に発生するかどうかをテストすることを意図していました。しかし、テスト自体がErrTooLargeが発生する前にシステムメモリを枯渇させてしまうという、皮肉な結果になっていました。

32ビットシステムとメモリ制限

32ビットシステムでは、アドレス空間が2^32バイト、つまり4GBに制限されます。この4GBの仮想アドレス空間は、通常、オペレーティングシステムとユーザープロセスで分割されます。一般的な構成では、ユーザープロセスが利用できるのは2GBまたは3GB程度です。

TestHuge関数が試みていた500GBというデータ量は、この32ビットシステムのアドレス空間をはるかに超えるため、テストが実行されるとすぐにメモリ割り当てエラー(OOM: Out Of Memory)が発生するか、システムが極端に遅くなるなどの問題を引き起こします。64ビットシステムではアドレス空間がはるかに広いため、このような直接的なアドレス空間の制限による問題は発生しにくいですが、それでも物理メモリやスワップ領域を使い果たす可能性はあります。

技術的詳細

削除されたTestHuge関数は、bytes.Bufferが非常に大きなデータを扱った場合に、ErrTooLargeエラーを適切に返すかどうかを検証することを目的としていました。このテストは、以下のロジックで構成されていました。

  1. testing.Short()チェック: go test -shortが指定された場合はテストをスキップします。
  2. deferによるパニックハンドリング: bytes.Bufferの操作中にErrTooLargeがパニックとして発生することを期待し、それを捕捉して検証します。もし他のエラーやパニックが発生した場合はテストを失敗させます。
  3. 巨大なバイトスライスの作成: big := make([]byte, 500e6)で500MBのバイトスライスを作成します。
  4. ループによる書き込み: for i := 0; i < 1000; i++ { b.Write(big) }というループで、この500MBのスライスを1000回、bytes.Bufferに書き込もうとします。これは合計で500MB * 1000 = 500GBのデータをバッファに書き込もうとする試みです。
  5. パニックの期待: ループが完了した場合(つまり、ErrTooLargeによるパニックが発生しなかった場合)は、テストを失敗させます。

このテストの技術的な問題点は、bytes.BufferErrTooLargeを返す前に、システムがメモリ不足に陥ってしまう点にありました。

  • メモリ割り当ての限界: Goのランタイムは、make([]byte, size)のようなスライス作成時に、指定されたサイズのメモリを確保しようとします。500MBのスライスを1000回書き込むということは、bytes.Bufferが内部的に500GBの容量を確保しようとすることを意味します。これは、たとえ64ビットシステムであっても、一般的な物理メモリ量をはるかに超えるため、OSレベルでのメモリ割り当て失敗(OOM Killerによるプロセス終了など)や、スワップ領域の枯渇、極端なパフォーマンス低下を引き起こします。
  • 32ビットシステムでの深刻化: 32ビットシステムでは、プロセスが利用できる仮想アドレス空間が最大4GB(実質2GB〜3GB)に制限されているため、500MBのスライスを数回書き込んだだけで、このアドレス空間を使い果たしてしまい、テストが正常にErrTooLargeを捕捉する前にクラッシュする可能性が非常に高くなります。
  • テストの信頼性の欠如: テストは、特定の条件(この場合はErrTooLargeの発生)を確実に検証できる必要があります。しかし、このTestHugeは、テスト環境のメモリ容量やOSのメモリ管理ポリシーに強く依存してしまい、再現性や信頼性が低いテストとなっていました。テストが不安定であると、CI/CDパイプラインでの誤った失敗通知や、開発者のローカル環境での再現性の問題など、多くの問題を引き起こします。

このような理由から、TestHugeは、その意図とは裏腹に、テストスイート全体の安定性を損なう要因となっていたため、削除が決定されました。

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

変更はsrc/pkg/bytes/buffer_test.goファイルのみです。 具体的には、以下のTestHuge関数全体が削除されました。

--- a/src/pkg/bytes/buffer_test.go
+++ b/src/pkg/bytes/buffer_test.go
@@ -386,24 +386,3 @@ func TestReadEmptyAtEOF(t *testing.T) {
 		t.Errorf("wrong count; got %d want 0", n)
 	}
 }
-
-func TestHuge(t *testing.T) {
-	// About to use tons of memory, so avoid for simple installation testing.
-	if testing.Short() {
-		return
-	}
-	// We expect a panic.
-	defer func() {
-		if err, ok := recover().(error); ok && err == ErrTooLarge {
-			return
-		} else {
-			t.Error(`expected "too large" error; got`, err)
-		}
-	}()
-	b := new(Buffer)
-	big := make([]byte, 500e6)
-	for i := 0; i < 1000; i++ {
-		b.Write(big)
-	}
-	t.Error("panic expected")
-}

コアとなるコードの解説

削除されたTestHuge関数は、bytes.Bufferが非常に大きなデータを扱った際に、ErrTooLargeというエラー(パニックとして発生)を適切に返すかどうかを検証するためのものでした。

関数内部では、まずtesting.Short()を使って、go test -shortが実行された場合にはテストをスキップするようにしていました。これは、このテストが大量のメモリを消費するため、通常の短いテスト実行時には除外したかったためです。

次に、defer文を使ってパニックを捕捉するロジックが記述されていました。これは、bytes.Bufferが容量の限界を超えた場合にErrTooLargeというパニックを発生させることを期待していたためです。捕捉されたパニックがErrTooLargeであればテストは成功とみなされ、そうでなければテストは失敗となります。

テストの本体では、new(Buffer)で新しいbytes.Bufferを作成し、make([]byte, 500e6)で500MBのバイトスライスbigを生成していました。そして、このbigスライスを1000回ループでb.Write(big)を使ってバッファに書き込もうとしていました。これにより、合計で500GBものデータをバッファに格納しようと試みていました。

最後に、ループが正常に完了してしまった場合(つまり、ErrTooLargeによるパニックが発生しなかった場合)には、t.Error("panic expected")でテストを失敗させていました。これは、500GBものデータをバッファに格納できるはずがなく、必ずErrTooLargeが発生することを期待していたためです。

しかし、前述の「変更の背景」や「技術的詳細」で述べたように、このテストはErrTooLargeがパニックとして発生する前に、システムがメモリ不足に陥るなどして不安定になることが判明しました。特に32ビットシステムでは、仮想メモリ空間の制限により、このテストが信頼性を持って実行できないという問題がありました。そのため、テストの目的は理解できるものの、その実装が環境に依存しすぎて信頼性に欠けるため、削除されることになりました。

関連リンク

参考にした情報源リンク

  • 特になし(コミット情報とGo言語の一般的な知識に基づく)