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

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

このコミットは、以前の変更 (CL 5504108 / 0edee03791f4) を元に戻すものです。元の変更は、エクスポートされる再帰的なインターフェースのサイズに制限を設けることで、過剰なメモリ消費を防ぎ、Issue #1909 を修正することを目的としていました。しかし、この変更が386アーキテクチャでのGoのビルドを破損させたため、その問題を解決するために元の状態に差し戻されました。

コミット

8a4bd094a033ceb00f7f5a504e4bc652ea5a164d

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

https://github.com/golang/go/commit/8a4bd094a033ceb00f7f5a504e4bc652ea5a164d

元コミット内容

undo CL 5504108 / 0edee03791f4

breaks 386 build

««« original CL description
gc: put limit on size of exported recursive interface

Prevents edge-case recursive types from consuming excessive memory.

Fixes #1909.

R=golang-dev, lvd, rsc
CC=golang-dev
https://golang.org/cl/5504108

»»»

R=lvd, lvd
CC=golang-dev
https://golang.org/cl/5534049

変更の背景

このコミットの背景には、Goコンパイラにおける再帰的な型(特にインターフェース)の処理に関する問題がありました。元のコミット (CL 5504108) は、特定の「エッジケース」において再帰的な型が無限に展開され、コンパイラが過剰なメモリを消費する可能性を修正するために導入されました。これは、GoのIssue #1909 で報告された問題に対処するためのものでした。

具体的には、コンパイラの型フォーマット処理において、再帰深度が深すぎる場合や、フォーマットされた出力のサイズが大きすぎる場合に、型の表示を省略する ("<...>") ロジックが追加されました。これにより、コンパイラの安定性とメモリ効率を向上させることが期待されました。

しかし、この変更が意図しない副作用を引き起こしました。特に、Intel 80386アーキテクチャ向けのビルド(386ビルド)が破損するという重大な問題が発生しました。386アーキテクチャは古い32ビットシステムであり、特定のコード変更や最適化がこの環境で互換性の問題を引き起こすことがあります。ビルドの破損は開発プロセスにおいて許容できない問題であるため、メモリ消費の最適化よりもビルドの安定性を優先し、問題の原因となった変更を元に戻すことが決定されました。

したがって、このコミットは、元の変更が引き起こしたビルドの破損を修正し、Goコンパイラの安定した動作を回復させることを目的としています。

前提知識の解説

このコミットを理解するためには、以下の技術的な概念とGo言語のコンパイラに関する知識が必要です。

  • Go言語のコンパイラ (gc): Go言語の公式コンパイラは gc と呼ばれ、Goのソースコードを機械語に変換する役割を担っています。src/cmd/gc ディレクトリには、このコンパイラのソースコードが含まれています。
  • 再帰的なインターフェース (Recursive Interface): Go言語において、インターフェースは型が満たすべきメソッドのセットを定義します。再帰的なインターフェースとは、インターフェースの定義の中に、そのインターフェース自身、またはそのインターフェースを含む別の型が参照されるような循環的な構造を持つインターフェースを指します。このような再帰的な型は、適切に処理されないと、コンパイラが無限ループに陥ったり、メモリを過剰に消費したりする原因となることがあります。
  • メモリ消費 (Memory Consumption): プログラムが実行中に使用するRAMの量です。コンパイラのような複雑なツールは、処理するコードの量や複雑さに応じて大量のメモリを消費することがあります。特に再帰的なデータ構造を扱う場合、無限に展開されることを防ぐためのガードレールがないと、メモリが枯渇する可能性があります。
  • 386ビルド (386 build): これは、Intel 80386プロセッサアーキテクチャ(一般的には32ビットシステム)向けにGoのコードをコンパイルすることを指します。Goはクロスコンパイルをサポートしており、異なるアーキテクチャ向けにバイナリを生成できます。386アーキテクチャは現代のシステムではあまり使われませんが、特定の組み込みシステムや古い環境では依然として重要です。このアーキテクチャ特有の制約やバグが存在することがあり、コード変更が特定のアーキテクチャでのみ問題を引き起こすことがあります。
  • CL (Change List): Goプロジェクトでは、Gerritというコードレビューシステムを使用して変更を管理しています。各変更は「Change List (CL)」として識別され、一意の番号が割り当てられます。https://golang.org/cl/ の後に続く番号がそれにあたります。
  • src/cmd/gc/fmt.c: このファイルはGoコンパイラ (gc) の一部であり、C言語で書かれています。ファイル名から推測されるように、コンパイラ内部での型情報のフォーマット(整形)や表示に関連するロジックが含まれている可能性が高いです。デバッグ出力、エラーメッセージ、または内部的な型表現の文字列化などに使用されます。
  • t->trecur: これは、Goコンパイラが型 (t) の再帰深度を追跡するために使用する内部フィールドであると推測されます。再帰的な型を処理する際に、無限ループを防ぐためにこの深度を監視します。
  • fp->nfmt: これは、フォーマット処理における出力のバイト数または文字数を追跡するためのフィールドであると推測されます。フォーマットされた文字列が特定のサイズを超えた場合に、処理を中断したり、省略したりするための条件として使用されることがあります。

技術的詳細

このコミットは、Goコンパイラの src/cmd/gc/fmt.c ファイル内の Tconv 関数に対する変更を元に戻すものです。Tconv 関数は、Goコンパイラが内部的に型情報を文字列に変換する際に使用される重要な関数です。これは、デバッグ出力、エラー報告、またはコンパイラの内部処理における型の表現に利用されます。

元のコミット (CL 5504108) では、再帰的なインターフェースが過剰なメモリを消費する問題を解決するために、Tconv 関数に以下の条件が追加されました。

if(t->trecur > 4 || fp->nfmt > 1000)
    return fmtstrcpy(fp, "<...>");

このコードは、以下のいずれかの条件が満たされた場合に、型の表示を省略して "<...>" と出力するように設計されていました。

  1. t->trecur > 4: 型の再帰深度が4を超えた場合。これは、無限再帰や非常に深い再帰構造を持つ型がコンパイラをフリーズさせたり、スタックオーバーフローを引き起こしたりするのを防ぐためのものです。
  2. fp->nfmt > 1000: フォーマットされた文字列の長さが1000バイト(または文字)を超えた場合。これは、非常に大きな型定義が原因で、デバッグ出力やエラーメッセージが過度に長くなり、メモリを消費したり、表示が困難になったりするのを防ぐためのものです。

しかし、この || fp->nfmt > 1000 という条件が、386アーキテクチャでのGoのビルドを破損させる原因となりました。具体的な破損のメカニズムはコミットメッセージからは明らかではありませんが、おそらく386アーキテクチャの特定のコンパイラ最適化、リンカの挙動、またはランタイムの特性と、この条件が組み合わさることで、予期せぬバグやクラッシュが発生したと考えられます。

今回のコミットでは、この問題に対処するために、問題の原因となった || fp->nfmt > 1000 の部分を削除し、コードを元の状態に戻しました。

if(t->trecur > 4)
    return fmtstrcpy(fp, "<...>");

これにより、型の表示を省略する条件は、再帰深度が4を超える場合に限定されることになります。この変更によって386ビルドの破損は解消されますが、同時に、フォーマットされた文字列のサイズによるメモリ消費の制限は一時的に解除されたことになります。これは、ビルドの安定性がメモリ最適化よりも優先された結果です。

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

--- a/src/cmd/gc/fmt.c
+++ b/src/cmd/gc/fmt.c
@@ -1450,7 +1450,7 @@ Tconv(Fmt *fp)
 	if(t == T)
 		return fmtstrcpy(fp, "<T>");

-	if(t->trecur > 4 || fp->nfmt > 1000)
+	if(t->trecur > 4)
 		return fmtstrcpy(fp, "<...>");

 	t->trecur++;

コアとなるコードの解説

上記のコードスニペットは、src/cmd/gc/fmt.c ファイル内の Tconv 関数の一部を示しています。

  • Tconv(Fmt *fp): この関数は、Goコンパイラが内部的な型 (t) をフォーマットされた文字列に変換するために使用されます。Fmt *fp は、フォーマット操作の状態を保持する構造体へのポインタであり、出力バッファや現在の出力サイズ (fp->nfmt) などの情報を含んでいます。
  • if(t == T): これは、型 t が特定の「T」という特殊な型である場合の処理です。この場合、<T> という文字列を返します。
  • if(t->trecur > 4 || fp->nfmt > 1000) (変更前):
    • t->trecur > 4: 現在処理している型の再帰深度が4を超えているかどうかをチェックします。Goコンパイラは、再帰的な型定義を処理する際に、無限ループに陥るのを防ぐために再帰深度を追跡します。この条件は、過度に深い再帰を検出した場合に発動します。
    • fp->nfmt > 1000: フォーマットされた出力の現在の長さが1000バイト(または文字)を超えているかどうかをチェックします。これは、非常に大きな型定義が原因で、生成される文字列が長くなりすぎるのを防ぐためのものです。
    • ||: 論理OR演算子であり、どちらか一方の条件が真であれば、続く return fmtstrcpy(fp, "<...>"); が実行されます。
    • return fmtstrcpy(fp, "<...>");: 型の表示を省略し、代わりに "<...>" という文字列を返します。これは、コンパイラが無限ループに陥ったり、過剰なメモリを消費したりするのを防ぐための防御的な措置です。
  • if(t->trecur > 4) (変更後):
    • このコミットによって、|| fp->nfmt > 1000 の部分が削除されました。
    • これにより、型の表示を省略する条件は、型の再帰深度が4を超える場合のみに限定されることになります。
    • この変更は、元のコミットが386ビルドを破損させた原因が fp->nfmt > 1000 の条件、またはその条件が引き起こす特定の挙動にあったことを示唆しています。この条件を削除することで、386ビルドの問題が解消されると判断されました。
  • t->trecur++;: 型の再帰深度をインクリメントします。これは、再帰的な呼び出しが行われる前に深度を増やし、関数から戻る際にデクリメントされる(または、この関数が再帰的に呼び出されるたびに深度が増える)ことで、再帰の深さを追跡します。

この変更は、Goコンパイラの安定性と特定のアーキテクチャでの互換性を確保するために、一時的にメモリ最適化の一部を犠牲にしたことを明確に示しています。

関連リンク

  • 元の変更リスト (CL 5504108): https://golang.org/cl/5504108
  • 関連するGo Issue: https://golang.org/issue/1909

参考にした情報源リンク

  • Go言語の公式ドキュメント (Goコンパイラに関する一般的な情報)
  • Gerrit Code Review (Goプロジェクトがコードレビューに使用しているシステム)
  • Intel 80386アーキテクチャに関する一般的な情報 (32ビットシステムとコンパイラの互換性について)