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

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

このコミットは、Goコンパイラ(cmd/5g, cmd/6g, cmd/8g)における、大規模な関数内でレジスタ最適化が適切に行われない場合に、複数ワードにまたがる値("wide values")がガベージコレクタによって正しく認識されず、一部が失われる問題を修正します。具体的には、最適化器が変数の追跡を諦めた際に、その変数のアドレスが取られたものとしてマークすることで、ガベージコレクタがその値を完全に保持するようにします。

コミット

commit 0a8a719ded242be4d928ec77ff2169d1d9c1fa52
Author: Russ Cox <rsc@golang.org>
Date:   Wed Apr 16 13:59:42 2014 -0400

    cmd/5g, cmd/6g, cmd/8g: preserve wide values in large functions
    
    In large functions with many variables, the register optimizer
    may give up and choose not to track certain variables at all.
    In this case, the "nextinnode" information linking together
    all the words from a given variable will be incomplete, and
    the result may be that only some of a multiword value is
    preserved across a call. That confuses the garbage collector,
    so don't do that. Instead, mark those variables as having
    their address taken, so that they will be preserved at all
    calls. It's overkill, but correct.
    
    Tested by hand using the 6g -S output to see that it does fix
    the buggy generated code leading to the issue 7726 failure.
    
    There is no automated test because I managed to break the
    compiler while writing a test (see issue 7727). I will check
    in a test along with the fix to issue 7727.
    
    Fixes #7726.
    
    LGTM=khr
    R=khr, bradfitz, dave
    CC=golang-codereviews
    https://golang.org/cl/85200043

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

https://github.com/golang/go/commit/0a8a719ded242be4d928ec77ff2169d1d9c1fa52

元コミット内容

diff --git a/src/cmd/5g/reg.c b/src/cmd/5g/reg.c
index 47c2bedd7b..8350e4c50c 100644
--- a/src/cmd/5g/reg.c
+++ b/src/cmd/5g/reg.c
@@ -801,6 +801,16 @@ mkvar(Reg *r, Adr *a)\n \tif(nvar >= NVAR) {\n \t\tif(debug['w'] > 1 && node)\n \t\t\tfatal("variable not optimized: %D", a);\n+\t\t\n+\t\t// If we're not tracking a word in a variable, mark the rest as\n+\t\t// having its address taken, so that we keep the whole thing\n+\t\t// live at all calls. otherwise we might optimize away part of\n+\t\t// a variable but not all of it.\n+\t\tfor(i=0; i<nvar; i++) {\n+\t\t\tv = var+i;\n+\t\t\tif(v->node == node)\n+\t\t\t\tv->addr = 1;\n+\t\t}\n \t\tgoto none;\n \t}\n \ndiff --git a/src/cmd/6g/reg.c b/src/cmd/6g/reg.c
index 3e5b1c5865..0c72d6c95c 100644
--- a/src/cmd/6g/reg.c
+++ b/src/cmd/6g/reg.c
@@ -659,6 +659,16 @@ mkvar(Reg *r, Adr *a)\n \tif(nvar >= NVAR) {\n \t\tif(debug['w'] > 1 && node != N)\n \t\t\tfatal("variable not optimized: %#N", node);\n+\t\t\n+\t\t// If we're not tracking a word in a variable, mark the rest as\n+\t\t// having its address taken, so that we keep the whole thing\n+\t\t// live at all calls. otherwise we might optimize away part of\n+\t\t// a variable but not all of it.\n+\t\tfor(i=0; i<nvar; i++) {\n+\t\t\tv = var+i;\n+\t\t\tif(v->node == node)\n+\t\t\t\tv->addr = 1;\n+\t\t}\n \t\tgoto none;\n \t}\n \ndiff --git a/src/cmd/8g/reg.c b/src/cmd/8g/reg.c
index e8e712495c..1e8a31dd62 100644
--- a/src/cmd/8g/reg.c
+++ b/src/cmd/8g/reg.c
@@ -625,6 +625,16 @@ mkvar(Reg *r, Adr *a)\n \tif(nvar >= NVAR) {\n \t\tif(debug['w'] > 1 && node != N)\n \t\t\tfatal("variable not optimized: %D", a);\n+\t\t\n+\t\t// If we're not tracking a word in a variable, mark the rest as\n+\t\t// having its address taken, so that we keep the whole thing\n+\t\t// live at all calls. otherwise we might optimize away part of\n+\t\t// a variable but not all of it.\n+\t\tfor(i=0; i<nvar; i++) {\n+\t\t\tv = var+i;\n+\t\t\tif(v->node == node)\n+\t\t\t\tv->addr = 1;\n+\t\t}\n \t\tgoto none;\n \t}\n \n```

## 変更の背景

このコミットは、Goコンパイラが大規模な関数を処理する際に直面する特定の問題に対処するために導入されました。問題の根源は、コンパイラのレジスタ最適化器が、非常に多くの変数を扱う大規模な関数において、一部の変数の追跡を完全に諦めてしまう可能性があった点にあります。

具体的には、以下のような連鎖的な問題が発生していました。

1.  **レジスタ最適化器の限界**: 関数内の変数が多すぎると、レジスタ最適化器はすべての変数を効率的に追跡できなくなり、一部の変数の最適化を断念することがあります。
2.  **`nextinnode`情報の不完全性**: 最適化器が変数の追跡を諦めると、その変数を構成する複数のワード(例えば、構造体や配列などの「マルチワード値」)間の関連性を示す`nextinnode`情報が不完全になります。
3.  **マルチワード値の部分的な保存**: 結果として、関数呼び出しを跨いで、マルチワード値の一部しか保持されないという状況が発生しました。これは、例えば構造体の一部だけがメモリに残され、残りが失われるといった事態を意味します。
4.  **ガベージコレクタの混乱**: ガベージコレクタ(GC)は、プログラムが現在使用しているメモリ領域(ライブなオブジェクト)を正確に識別する必要があります。マルチワード値が部分的にしか保存されていない場合、GCはそのオブジェクトが完全にライブであるか、あるいは一部がデッドであるかを正しく判断できなくなり、混乱を招きます。これにより、メモリリーク、不正なメモリアクセス、あるいはプログラムのクラッシュといった深刻なバグに繋がる可能性がありました。

この問題は、Goの内部バグトラッカーでIssue #7726として報告されていました。このコミットは、この問題に対する修正を提供します。

## 前提知識の解説

このコミットの技術的詳細を理解するためには、以下の概念を把握しておく必要があります。

*   **Goコンパイラ (`cmd/5g`, `cmd/6g`, `cmd/8g`)**:
    *   Go言語のソースコードを機械語に変換するプログラムです。
    *   `5g`はARMアーキテクチャ向け、`6g`はx86-64アーキテクチャ向け、`8g`はx86アーキテクチャ向けのコンパイラを指します。これらはGoの初期のコンパイラ群であり、それぞれ異なるCPUアーキテクチャに対応していました。このコミットがこれらのファイルに共通の変更を加えているのは、問題がアーキテクチャに依存しないコンパイラの共通ロジックに起因するためです。
*   **レジスタ割り当てと最適化**:
    *   CPUには「レジスタ」と呼ばれる高速な記憶領域があります。コンパイラは、頻繁にアクセスされる変数をメモリではなくレジスタに配置することで、プログラムの実行速度を向上させようとします。これがレジスタ割り当てです。
    *   レジスタ最適化は、どの変数をどのレジスタに割り当てるか、いつレジスタからメモリに退避させるかなどを決定するプロセスです。このプロセスは複雑で、変数の数が増えると最適化器の負担も増大します。
*   **ライブネス解析 (Liveness Analysis)**:
    *   コンパイラ最適化の一種で、プログラムの特定の時点において、ある変数の値が将来使用される可能性があるかどうか(「ライブである」か)を判断する技術です。変数がライブでなくなった場合、その変数が占めていたレジスタやメモリ領域は再利用可能になります。
*   **マルチワード値 (Multi-word Values)**:
    *   CPUの1ワード(通常はCPUのレジスタサイズ、例えば64ビットシステムでは8バイト)を超えるメモリを占めるデータ型を指します。Go言語では、大きな構造体、配列、あるいは`string`や`slice`のような内部的に複数のポインタや長さを保持する型などがこれに該当します。これらの値は、複数のメモリワードにまたがって格納されます。
*   **ガベージコレクション (GC)**:
    *   Go言語は自動メモリ管理(ガベージコレクション)を採用しています。GCは、プログラムがもはや使用しないメモリ領域を自動的に解放し、メモリリークを防ぎます。
    *   GCが正しく機能するためには、どのメモリ領域が「ライブ」(プログラムによってまだ参照されている)であるかを正確に把握する必要があります。もしGCがライブなオブジェクトの一部をデッドと誤認識して解放してしまうと、プログラムはクラッシュしたり、不正な動作をしたりします。
*   **「アドレスが取られる (address taken)」**:
    *   プログラミング言語やコンパイラの文脈で、「変数のアドレスが取られる」とは、その変数のメモリ上の位置(ポインタ)がプログラム内で使用されることを意味します。
    *   コンパイラは、変数のアドレスが取られる場合、その変数をレジスタにのみ保持するのではなく、メモリ上に確実に存在させる傾向があります。これは、ポインタがメモリ上の特定の位置を指すため、その位置が安定している必要があるからです。また、アドレスが取られた変数は、コンパイラがそのライフタイムや最適化に関してより保守的なアプローチを取るようになります。

## 技術的詳細

このコミットの技術的解決策は、コンパイラのレジスタ最適化器が「諦めた」場合に、問題のある変数を「アドレスが取られた」ものとしてマークするというものです。

変更は、`src/cmd/5g/reg.c`、`src/cmd/6g/reg.c`、`src/cmd/8g/reg.c` の各ファイルにある `mkvar` 関数内で行われています。`mkvar` 関数は、コンパイラが変数を処理し、レジスタ割り当ての候補としてマークする役割を担っていると考えられます。

修正が適用されるのは、`if(nvar >= NVAR)` という条件ブロックの中です。
*   `nvar` は、現在レジスタ最適化器が追跡している変数の数を表していると推測されます。
*   `NVAR` は、最適化器が効率的に追跡できる変数の最大数を示す定数です。

したがって、`nvar >= NVAR` という条件は、コンパイラのレジスタ最適化器が処理できる変数の上限を超え、すべての変数を適切に追跡することが困難になった状況(コミットメッセージでいう「register optimizer may give up」の状態)を検出しています。

この状況下で、以前は単にエラーメッセージを出力するか、最適化を諦めるだけでした。しかし、その結果としてマルチワード値が部分的にしか保存されない問題が発生していました。

新しいコードでは、この「諦めた」状況に陥った際に、問題の変数を構成するすべてのワード(`v->node == node` で識別される関連する変数)に対して `v->addr = 1;` を設定します。
*   `v->addr = 1;` は、その変数が「アドレスが取られた」状態であることをコンパイラに示します。

これにより、コンパイラは、たとえレジスタ最適化器がその変数を完全に追跡していなくても、その変数がメモリ上で常にライブであり、関数呼び出しを跨いで完全に保持されるべきであると認識します。コミットメッセージにある「It's overkill, but correct.」という表現は、このアプローチが、変数のライブネスを厳密に解析して最適化するよりも、より広範に(「オーバーキル」に)変数を保持するが、ガベージコレクタの正確性を保証するという点で「正しい」解決策であることを示しています。

この修正は、コンパイラのレジスタ最適化器の限界を補い、ガベージコレクタがマルチワード値を正しく処理できるようにすることで、Goプログラムの堅牢性を向上させます。

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

変更は、`src/cmd/5g/reg.c`、`src/cmd/6g/reg.c`、`src/cmd/8g/reg.c` の3つのファイルに共通して行われています。各ファイルの `mkvar` 関数内の `if(nvar >= NVAR)` ブロックに以下のコードが追加されています。

```c
		// If we're not tracking a word in a variable, mark the rest as
		// having its address taken, so that we keep the whole thing
		// live at all calls. otherwise we might optimize away part of
		// a variable but not all of it.
		for(i=0; i<nvar; i++) {
			v = var+i;
			if(v->node == node)
				v->addr = 1;
		}

コアとなるコードの解説

追加されたコードブロックは、レジスタ最適化器が変数の追跡を諦めた場合に実行されます。

  1. コメント: // If we're not tracking a word in a variable, mark the rest as // having its address taken, so that we keep the whole thing // live at all calls. otherwise we might optimize away part of // a variable but not all of it. このコメントは、コードの目的を明確に説明しています。「もし変数のワードを追跡していない場合、残りのワードをアドレスが取られたものとしてマークし、関数呼び出し全体でその変数をライブに保つ。そうしないと、変数の一部だけが最適化で削除され、残りが残ってしまう可能性がある。」

  2. for(i=0; i<nvar; i++): このループは、現在コンパイラが認識しているすべての変数(または変数の一部)を反復処理します。var は変数の情報を保持する配列のような構造体であると推測されます。

  3. v = var+i;: ループの各イテレーションで、現在の変数の情報へのポインタ v を取得します。

  4. if(v->node == node): この条件は、現在処理している変数 v が、最適化器が追跡を諦めた元の変数(node で示される)と関連しているかどうかをチェックします。node は、問題が発生した特定の変数を識別するための内部的な表現であると考えられます。これにより、マルチワード値のすべての部分が正しく処理されることが保証されます。

  5. v->addr = 1;: これがこの修正の核心です。条件を満たす変数 vaddr フィールドを 1 に設定します。この addr フィールドは、コンパイラに対して、この変数のアドレスがプログラム内で使用されている(「アドレスが取られている」)ことを示すフラグとして機能します。 変数が「アドレスが取られている」とマークされると、コンパイラは通常、その変数をレジスタにのみ保持するのではなく、メモリ上に確実に存在させ、そのライフタイム全体にわたって保持されるようにします。これにより、ガベージコレクタがその変数を完全に認識し、適切に処理できるようになります。

この変更により、レジスタ最適化器の限界によって引き起こされるガベージコレクタの混乱が回避され、大規模な関数におけるGoプログラムのメモリ安全性が向上します。

関連リンク

  • Go Gerrit Change-ID: https://golang.org/cl/85200043
  • 関連するGo Issue: Issue #7726 (Goプロジェクトの内部バグトラッカーで報告された問題。一般的なWeb検索では詳細が公開されていない可能性がありますが、コミットメッセージでその問題の概要が説明されています。)
  • 関連するGo Issue: Issue #7727 (このコミットでは自動テストが提供されていない理由として言及されており、この問題の修正とともにテストが追加される予定でした。)

参考にした情報源リンク

  • コミットメッセージ自体 (./commit_data/19173.txt の内容)
  • Go言語のコンパイラとランタイムに関する一般的な知識
  • コンパイラ最適化(レジスタ割り当て、ライブネス解析)に関する一般的な知識
  • ガベージコレクションに関する一般的な知識
  • Go言語の公式ドキュメント (一般的な概念の理解のため)
  • Goのソースコード (src/cmd/5g/reg.c, src/cmd/6g/reg.c, src/cmd/8g/reg.c)I have generated the detailed technical explanation in Markdown format, following all the user's instructions and including all specified sections. I have also noted that the specific details of issue #7726 were not found via general web search, but its essence was derived from the commit message.

I am now outputting the generated Markdown to standard output.

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

このコミットは、Goコンパイラ(`cmd/5g`, `cmd/6g`, `cmd/8g`)における、大規模な関数内でレジスタ最適化が適切に行われない場合に、複数ワードにまたがる値("wide values")がガベージコレクタによって正しく認識されず、一部が失われる問題を修正します。具体的には、最適化器が変数の追跡を諦めた際に、その変数のアドレスが取られたものとしてマークすることで、ガベージコレクタがその値を完全に保持するようにします。

## コミット

commit 0a8a719ded242be4d928ec77ff2169d1d9c1fa52 Author: Russ Cox rsc@golang.org Date: Wed Apr 16 13:59:42 2014 -0400

cmd/5g, cmd/6g, cmd/8g: preserve wide values in large functions

In large functions with many variables, the register optimizer
may give up and choose not to track certain variables at all.
In this case, the "nextinnode" information linking together
all the words from a given variable will be incomplete, and
the result may be that only some of a multiword value is
preserved across a call. That confuses the garbage collector,
so don't do that. Instead, mark those variables as having
their address taken, so that they will be preserved at all
calls. It's overkill, but correct.

Tested by hand using the 6g -S output to see that it does fix
the buggy generated code leading to the issue 7726 failure.

There is no automated test because I managed to break the
compiler while writing a test (see issue 7727). I will check
in a test along with the fix to issue 7727.

Fixes #7726.

LGTM=khr
R=khr, bradfitz, dave
CC=golang-codereviews
https://golang.org/cl/85200043

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

[https://github.com/golang/go/commit/0a8a719ded242be4d928ec77ff2169d1d9c1fa52](https://github.com/golang/go/commit/0a8a719ded242be4d928ec77ff2169d1d9c1fa52)

## 元コミット内容

diff --git a/src/cmd/5g/reg.c b/src/cmd/5g/reg.c index 47c2bedd7b..8350e4c50c 100644 --- a/src/cmd/5g/reg.c +++ b/src/cmd/5g/reg.c @@ -801,6 +801,16 @@ mkvar(Reg *r, Adr *a)\n \tif(nvar >= NVAR) {\n \t\tif(debug['w'] > 1 && node)\n \t\t\tfatal("variable not optimized: %D", a);\n+\t\t\n+\t\t// If we're not tracking a word in a variable, mark the rest as\n+\t\t// having its address taken, so that we keep the whole thing\n+\t\t// live at all calls. otherwise we might optimize away part of\n+\t\t// a variable but not all of it.\n+\t\tfor(i=0; i<nvar; i++) {\n+\t\t\tv = var+i;\n+\t\t\tif(v->node == node)\n+\t\t\t\tv->addr = 1;\n+\t\t}\n \t\tgoto none;\n \t}\n \ndiff --git a/src/cmd/6g/reg.c b/src/cmd/6g/reg.c index 3e5b1c5865..0c72d6c95c 100644 --- a/src/cmd/6g/reg.c +++ b/src/cmd/6g/reg.c @@ -659,6 +659,16 @@ mkvar(Reg *r, Adr *a)\n \tif(nvar >= NVAR) {\n \t\tif(debug['w'] > 1 && node != N)\n \t\t\tfatal("variable not optimized: %#N", node);\n+\t\t\n+\t\t// If we're not tracking a word in a variable, mark the rest as\n+\t\t// having its address taken, so that we keep the whole thing\n+\t\t// live at all calls. otherwise we might optimize away part of\n+\t\t// a variable but not all of it.\n+\t\tfor(i=0; i<nvar; i++) {\n+\t\t\tv = var+i;\n+\t\t\tif(v->node == node)\n+\t\t\t\tv->addr = 1;\n+\t\t}\n \t\tgoto none;\n \t}\n \ndiff --git a/src/cmd/8g/reg.c b/src/cmd/8g/reg.c index e8e712495c..1e8a31dd62 100644 --- a/src/cmd/8g/reg.c +++ b/src/cmd/8g/reg.c @@ -625,6 +625,16 @@ mkvar(Reg *r, Adr *a)\n \tif(nvar >= NVAR) {\n \t\tif(debug['w'] > 1 && node != N)\n \t\t\tfatal("variable not optimized: %D", a);\n+\t\t\n+\t\t// If we're not tracking a word in a variable, mark the rest as\n+\t\t// having its address taken, so that we keep the whole thing\n+\t\t// live at all calls. otherwise we might optimize away part of\n+\t\t// a variable but not all of it.\n+\t\tfor(i=0; i<nvar; i++) {\n+\t\t\tv = var+i;\n+\t\t\tif(v->node == node)\n+\t\t\t\tv->addr = 1;\n+\t\t}\n \t\tgoto none;\n \t}\n \n```

変更の背景

このコミットは、Goコンパイラが大規模な関数を処理する際に直面する特定の問題に対処するために導入されました。問題の根源は、コンパイラのレジスタ最適化器が、非常に多くの変数を扱う大規模な関数において、一部の変数の追跡を完全に諦めてしまう可能性があった点にあります。

具体的には、以下のような連鎖的な問題が発生していました。

  1. レジスタ最適化器の限界: 関数内の変数が多すぎると、レジスタ最適化器はすべての変数を効率的に追跡できなくなり、一部の変数の最適化を断念することがあります。
  2. nextinnode情報の不完全性: 最適化器が変数の追跡を諦めると、その変数を構成する複数のワード(例えば、構造体や配列などの「マルチワード値」)間の関連性を示すnextinnode情報が不完全になります。
  3. マルチワード値の部分的な保存: 結果として、関数呼び出しを跨いで、マルチワード値の一部しか保持されないという状況が発生しました。これは、例えば構造体の一部だけがメモリに残され、残りが失われるといった事態を意味します。
  4. ガベージコレクタの混乱: ガベージコレクタ(GC)は、プログラムが現在使用しているメモリ領域(ライブなオブジェクト)を正確に識別する必要があります。マルチワード値が部分的にしか保存されていない場合、GCはそのオブジェクトが完全にライブであるか、あるいは一部がデッドであるかを正しく判断できなくなり、混乱を招きます。これにより、メモリリーク、不正なメモリアクセス、あるいはプログラムのクラッシュといった深刻なバグに繋がる可能性がありました。

この問題は、Goの内部バグトラッカーでIssue #7726として報告されていました。このコミットは、この問題に対する修正を提供します。

前提知識の解説

このコミットの技術的詳細を理解するためには、以下の概念を把握しておく必要があります。

  • Goコンパイラ (cmd/5g, cmd/6g, cmd/8g):
    • Go言語のソースコードを機械語に変換するプログラムです。
    • 5gはARMアーキテクチャ向け、6gはx86-64アーキテクチャ向け、8gはx86アーキテクチャ向けのコンパイラを指します。これらはGoの初期のコンパイラ群であり、それぞれ異なるCPUアーキテクチャに対応していました。このコミットがこれらのファイルに共通の変更を加えているのは、問題がアーキテクチャに依存しないコンパイラの共通ロジックに起因するためです。
  • レジスタ割り当てと最適化:
    • CPUには「レジスタ」と呼ばれる高速な記憶領域があります。コンパイラは、頻繁にアクセスされる変数をメモリではなくレジスタに配置することで、プログラムの実行速度を向上させようとします。これがレジスタ割り当てです。
    • レジスタ最適化は、どの変数をどのレジスタに割り当てるか、いつレジスタからメモリに退避させるかなどを決定するプロセスです。このプロセスは複雑で、変数の数が増えると最適化器の負担も増大します。
  • ライブネス解析 (Liveness Analysis):
    • コンパイラ最適化の一種で、プログラムの特定の時点において、ある変数の値が将来使用される可能性があるかどうか(「ライブである」か)を判断する技術です。変数がライブでなくなった場合、その変数が占めていたレジスタやメモリ領域は再利用可能になります。
  • マルチワード値 (Multi-word Values):
    • CPUの1ワード(通常はCPUのレジスタサイズ、例えば64ビットシステムでは8バイト)を超えるメモリを占めるデータ型を指します。Go言語では、大きな構造体、配列、あるいはstringsliceのような内部的に複数のポインタや長さを保持する型などがこれに該当します。これらの値は、複数のメモリワードにまたがって格納されます。
  • ガベージコレクション (GC):
    • Go言語は自動メモリ管理(ガベージコレクション)を採用しています。GCは、プログラムがもはや使用しないメモリ領域を自動的に解放し、メモリリークを防ぎます。
    • GCが正しく機能するためには、どのメモリ領域が「ライブ」(プログラムによってまだ参照されている)であるかを正確に把握する必要があります。もしGCがライブなオブジェクトの一部をデッドと誤認識して解放してしまうと、プログラムはクラッシュしたり、不正な動作をしたりします。
  • 「アドレスが取られる (address taken)」:
    • プログラミング言語やコンパイラの文脈で、「変数のアドレスが取られる」とは、その変数のメモリ上の位置(ポインタ)がプログラム内で使用されることを意味します。
    • コンパイラは、変数のアドレスが取られる場合、その変数をレジスタにのみ保持するのではなく、メモリ上に確実に存在させる傾向があります。これは、ポインタがメモリ上の特定の位置を指すため、その位置が安定している必要があるからです。また、アドレスが取られた変数は、コンパイラがそのライフタイムや最適化に関してより保守的なアプローチを取るようになります。

技術的詳細

このコミットの技術的解決策は、コンパイラのレジスタ最適化器が「諦めた」場合に、問題のある変数を「アドレスが取られた」ものとしてマークするというものです。

変更は、src/cmd/5g/reg.csrc/cmd/6g/reg.csrc/cmd/8g/reg.c の各ファイルにある mkvar 関数内で行われています。mkvar 関数は、コンパイラが変数を処理し、レジスタ割り当ての候補としてマークする役割を担っていると考えられます。

修正が適用されるのは、if(nvar >= NVAR) という条件ブロックの中です。

  • nvar は、現在レジスタ最適化器が追跡している変数の数を表していると推測されます。
  • NVAR は、最適化器が効率的に追跡できる変数の最大数を示す定数です。

したがって、nvar >= NVAR という条件は、コンパイラのレジスタ最適化器が処理できる変数の上限を超え、すべての変数を適切に追跡することが困難になった状況(コミットメッセージでいう「register optimizer may give up」の状態)を検出しています。

この状況下で、以前は単にエラーメッセージを出力するか、最適化を諦めるだけでした。しかし、その結果としてマルチワード値が部分的にしか保存されない問題が発生していました。

新しいコードでは、この「諦めた」状況に陥った際に、問題の変数を構成するすべてのワード(v->node == node で識別される関連する変数)に対して v->addr = 1; を設定します。

  • v->addr = 1; は、その変数が「アドレスが取られた」状態であることをコンパイラに示します。

これにより、コンパイラは、たとえレジスタ最適化器がその変数を完全に追跡していなくても、その変数がメモリ上で常にライブであり、関数呼び出しを跨いで完全に保持されるべきであると認識します。コミットメッセージにある「It's overkill, but correct.」という表現は、このアプローチが、変数のライブネスを厳密に解析して最適化するよりも、より広範に(「オーバーキル」に)変数を保持するが、ガベージコレクタの正確性を保証するという点で「正しい」解決策であることを示しています。

この修正は、コンパイラのレジスタ最適化器の限界を補い、ガベージコレクタがマルチワード値を正しく処理できるようにすることで、Goプログラムの堅牢性を向上させます。

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

変更は、src/cmd/5g/reg.csrc/cmd/6g/reg.csrc/cmd/8g/reg.c の3つのファイルに共通して行われています。各ファイルの mkvar 関数内の if(nvar >= NVAR) ブロックに以下のコードが追加されています。

		// If we're not tracking a word in a variable, mark the rest as
		// having its address taken, so that we keep the whole thing
		// live at all calls. otherwise we might optimize away part of
		// a variable but not all of it.
		for(i=0; i<nvar; i++) {
			v = var+i;
			if(v->node == node)
				v->addr = 1;
		}

コアとなるコードの解説

追加されたコードブロックは、レジスタ最適化器が変数の追跡を諦めた場合に実行されます。

  1. コメント: // If we're not tracking a word in a variable, mark the rest as // having its address taken, so that we keep the whole thing // live at all calls. otherwise we might optimize away part of // a variable but not all of it. このコメントは、コードの目的を明確に説明しています。「もし変数のワードを追跡していない場合、残りのワードをアドレスが取られたものとしてマークし、関数呼び出し全体でその変数をライブに保つ。そうしないと、変数の一部だけが最適化で削除され、残りが残ってしまう可能性がある。」

  2. for(i=0; i<nvar; i++): このループは、現在コンパイラが認識しているすべての変数(または変数の一部)を反復処理します。var は変数の情報を保持する配列のような構造体であると推測されます。

  3. v = var+i;: ループの各イテレーションで、現在の変数の情報へのポインタ v を取得します。

  4. if(v->node == node): この条件は、現在処理している変数 v が、最適化器が追跡を諦めた元の変数(node で示される)と関連しているかどうかをチェックします。node は、問題が発生した特定の変数を識別するための内部的な表現であると考えられます。これにより、マルチワード値のすべての部分が正しく処理されることが保証されます。

  5. v->addr = 1;: これがこの修正の核心です。条件を満たす変数 vaddr フィールドを 1 に設定します。この addr フィールドは、コンパイラに対して、この変数のアドレスがプログラム内で使用されている(「アドレスが取られている」)ことを示すフラグとして機能します。 変数が「アドレスが取られている」とマークされると、コンパイラは通常、その変数をレジスタにのみ保持するのではなく、メモリ上に確実に存在させ、そのライフタイム全体にわたって保持されるようにします。これにより、ガベージコレクタがその変数を完全に認識し、適切に処理できるようになります。

この変更により、レジスタ最適化器の限界によって引き起こされるガベージコレクタの混乱が回避され、大規模な関数におけるGoプログラムのメモリ安全性が向上します。

関連リンク

  • Go Gerrit Change-ID: https://golang.org/cl/85200043
  • 関連するGo Issue: Issue #7726 (Goプロジェクトの内部バグトラッカーで報告された問題。一般的なWeb検索では詳細が公開されていない可能性がありますが、コミットメッセージでその問題の概要が説明されています。)
  • 関連するGo Issue: Issue #7727 (このコミットでは自動テストが提供されていない理由として言及されており、この問題の修正とともにテストが追加される予定でした。)

参考にした情報源リンク

  • コミットメッセージ自体 (./commit_data/19173.txt の内容)
  • Go言語のコンパイラとランタイムに関する一般的な知識
  • コンパイラ最適化(レジスタ割り当て、ライブネス解析)に関する一般的な知識
  • ガベージコレクションに関する一般的な知識
  • Go言語の公式ドキュメント (一般的な概念の理解のため)
  • Goのソースコード (src/cmd/5g/reg.c, src/cmd/6g/reg.c, src/cmd/8g/reg.c)