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

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

このコミットは、Goコンパイラ(cmd/gc)における競合状態のコンパイルエラー「non-orig name」を修正するものです。特に、sinit.cファイル内のstaticcopy関数におけるNode*の扱いに関する問題に対処しています。

コミット

commit 8a2db409c4e08ec9a8d87bdcaea928083f6293fc
Author: Russ Cox <rsc@golang.org>
Date:   Tue May 27 23:59:27 2014 -0400

    cmd/gc: fix race compilation failure 'non-orig name'
    
    CL 51010045 fixed the first one of these:
    
            cmd/gc: return canonical Node* from temp
    
            For historical reasons, temp was returning a copy
            of the created Node*, not the original Node*.
            This meant that if analysis recorded information in the
            returned node (for example, n->addrtaken = 1), the
            analysis would not show up on the original Node*, the
            one kept in fn->dcl and consulted during liveness
            bitmap creation.
    
            Correct this, and watch for it when setting addrtaken.
    
            Fixes #7083.
    
            R=khr, dave, minux.ma
            CC=golang-codereviews
            https://golang.org/cl/51010045
    
    CL 53200043 fixed the second:
    
            cmd/gc: fix race build
    
            Missed this case in CL 51010045.
    
            TBR=khr
            CC=golang-codereviews
            https://golang.org/cl/53200043
    
    This CL fixes the third. There are only three nod(OXXX, ...)
    calls in sinit.c, so maybe we're done. Embarassing that it
    took three CLs to find all three.
    
    Fixes #8028.
    
    LGTM=khr
    R=golang-codereviews, khr
    CC=golang-codereviews, iant
    https://golang.org/cl/100800046

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

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

元コミット内容

このコミットは、Goコンパイラ(cmd/gc)が競合検出モード(-raceフラグ)でコンパイルする際に発生する「non-orig name」というコンパイルエラーを修正します。このエラーは、コンパイラが内部的に生成するノード(Node*)の「オリジナル」ではないコピーを扱ってしまうことに起因していました。

具体的には、sinit.cファイル内のnod(OXXX, ...)という形式のノード生成呼び出しが問題を引き起こしていました。このコミットは、同様の問題を修正した過去の2つの変更(CL 51010045とCL 53200043)に続く、3番目の修正となります。コミットメッセージでは、sinit.cにはこのような呼び出しが3つしかないため、これで全ての問題が解決された可能性に言及しつつ、3つのCLに分けて修正されたことへの「恥ずかしさ」を表明しています。

この修正は、Issue #8028を解決します。

変更の背景

Goコンパイラには、データ競合を検出するための「レース検出器(Race Detector)」が組み込まれています。この機能は、プログラムの実行時にメモリへの同時アクセスを監視し、潜在的な競合状態を報告することで、並行プログラミングにおけるバグの特定を支援します。レース検出器は、コンパイル時に特定のインストゥルメンテーションコードを挿入することで機能します。

このコミットの背景にある問題は、コンパイラの内部表現、特に抽象構文木(AST)のノード(Node*)の扱いにありました。Goコンパイラは、ソースコードを解析してASTを構築し、そのASTに対して様々な最適化や解析を行います。レース検出器もこの解析フェーズで、変数のアドレスが取られているかどうか(addrtakenフラグ)などの情報をノードに記録します。

しかし、過去のコンパイラの設計では、temp関数(一時変数を生成する関数)などが、作成されたNode*の「コピー」を返していました。この「コピー」は、元のノードとは異なるメモリ上のエンティティでありながら、論理的には同じ変数を指していました。このため、解析フェーズでコピーされたノードにaddrtaken = 1のような情報が記録されても、コンパイラが後で参照する「元のノード」(例えば、関数の宣言リストfn->dclに保持されているノードや、ライブネスビットマップの生成時に参照されるノード)には、その情報が反映されませんでした。

この不整合が、レース検出器が期待する情報と実際のノードの状態との間にギャップを生じさせ、「non-orig name」というコンパイルエラーを引き起こしていました。これは、レース検出器が、本来アドレスが取られているとマークされているべき変数について、その情報が欠落しているために、不正な状態を検出したことを示唆しています。

このコミットは、同様の問題を修正した以下の2つの先行コミットに続くものです。

  • CL 51010045 (cmd/gc: return canonical Node* from temp): temp関数が、作成されたNode*のコピーではなく、正規のNode*を返すように修正しました。これにより、addrtakenなどの解析情報が元のノードに正しく反映されるようになりました。
  • CL 53200043 (cmd/gc: fix race build): CL 51010045で修正しきれなかった別のケースを修正しました。

本コミットは、sinit.c内の残りの同様の問題を解決し、この種の「non-orig name」エラーを完全に解消することを目的としています。

前提知識の解説

このコミットを理解するためには、以下のGoコンパイラの内部構造と概念に関する知識が必要です。

  1. cmd/gc: Go言語の公式コンパイラの一部であり、Goソースコードを機械語に変換する主要なコンポーネントです。Goコンパイラは、フロントエンド(字句解析、構文解析、AST構築)、ミドルエンド(型チェック、最適化、エスケープ解析など)、バックエンド(コード生成)から構成されます。
  2. 抽象構文木(AST)とNode*: コンパイラはソースコードを解析し、プログラムの構造を木構造で表現した抽象構文木(AST)を構築します。ASTの各要素は「ノード」と呼ばれ、Goコンパイラ内部ではNode構造体(またはそのポインタNode*)で表現されます。変数、定数、式、文、関数宣言など、プログラムのあらゆる要素がノードとして表現されます。
  3. Node->origフィールド: Node構造体には、origというフィールドが存在します。このフィールドは、ノードが元のソースコードのどの部分に対応するか、あるいはノードがコピーや変換によって生成された場合に、その「オリジナル」のノードを指し示すために使用されます。コンパイラの解析フェーズでは、このorigフィールドを介して、論理的に同じエンティティ(例えば、同じ変数)に関連するすべてのノードが、一貫した情報(例: addrtakenフラグ)を持つことが重要になります。
  4. addrtakenフラグ: Node構造体には、addrtakenというフラグ(通常はビットフィールド)があります。このフラグは、そのノードが表す変数のアドレスがプログラム内で取得されたかどうかを示します。アドレスが取られた変数は、ポインタを介してアクセスされる可能性があるため、レース検出器やエスケープ解析(変数がヒープに割り当てられるべきかスタックに割り当てられるべきかを決定する解析)にとって重要な情報となります。
  5. レース検出器(Race Detector): Go言語に組み込まれている強力なツールで、並行プログラムにおけるデータ競合を検出します。コンパイル時に-raceフラグを付けてビルドすると、コンパイラはメモリアクセスを監視する追加のコードを生成します。この監視コードは、実行時に複数のゴルーチンが同時に同じメモリ位置にアクセスし、少なくとも一方が書き込み操作である場合に警告を発します。レース検出器が正しく機能するためには、コンパイラが変数の特性(例: addrtaken)を正確に把握している必要があります。
  6. sinit.c: Goコンパイラのソースコードの一部で、主に静的初期化(static initialization)に関連する処理を扱うファイルです。グローバル変数やパッケージレベルの変数の初期化、特に複雑な初期化式を持つ場合の処理が含まれます。このファイル内で、コンパイラは一時的なノードを生成したり、既存のノードをコピーしたりする操作を行うことがあります。
  7. nod(OXXX, ...): cmd/gcにおけるノード生成関数の一般的なパターンです。nodは「node」の略で、OXXXはノードのオペレーションタイプ(例: ONAMEは変数名、OADDは加算など)を示します。この関数は、指定されたオペレーションタイプとオペランドを持つ新しいASTノードを作成します。

技術的詳細

このコミットが修正する問題は、Goコンパイラの内部でASTノードがコピーされる際に、そのコピーが「オリジナル」のノードとの関連性を失ってしまうことにありました。特に、sinit.c内のstaticcopy関数が関与していました。

staticcopy関数は、静的初期化のコンテキストでノードをコピーする際に使用されます。元のコードでは、nod(OXXX, N, N)を呼び出して新しいノードllを作成し、その後*ll = n1;という代入によって、既存のノードn1の内容をllにコピーしていました。

この*ll = n1;という操作は、n1の構造体メンバーをllにビット単位でコピーします。これには、n1origフィールドも含まれます。しかし、n1自体が既にコピーされたノードである場合、そのorigフィールドはさらに別の「オリジナル」を指している可能性があります。問題は、lln1の「コピー」として作成されたにもかかわらず、ll自身のorigフィールドがll自身を指すように設定されていなかった点にありました。

レース検出器などのコンパイラ解析は、ノードのorigフィールドを辿って、そのノードが表す論理的なエンティティ(変数など)の「真のオリジナル」に到達し、そこに記録されたaddrtakenなどの情報を参照します。もしlln1のコピーであり、n1がさらに別のノードのコピーである場合、llorigフィールドがn1origフィールドを指していると、解析は正しい「オリジナル」に到達できないか、あるいは誤ったノードに情報を記録してしまう可能性がありました。

このコミットの修正は、ll->orig = ll;という一行を追加することで、この問題を解決しています。この行は、lln1の内容をコピーされた後、ll自身のorigフィールドをll自身に設定し直します。これにより、llは「完全に独立したコピー」として扱われるようになります。つまり、lln1の構造を模倣しつつも、そのorigフィールドはll自身を指すため、llに対する解析結果(例: addrtaken)は、ll自身に直接関連付けられ、他のノードのorigチェーンに影響を与えることなく、独立して処理されるようになります。

この変更により、レース検出器がllのような一時的に生成されたノードを解析する際に、そのノードが表す変数のaddrtaken情報などを正しく追跡できるようになり、「non-orig name」エラーが解消されます。

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

変更は以下の2つのファイルで行われています。

  1. src/cmd/gc/sinit.c:

    --- a/src/cmd/gc/sinit.c
    +++ b/src/cmd/gc/sinit.c
    @@ -354,6 +354,7 @@ staticcopy(Node *l, Node *r, NodeList **out)
     			else {
     				ll = nod(OXXX, N, N);
     				*ll = n1;
    +				ll->orig = ll; // completely separate copy
     				if(!staticassign(ll, e->expr, out)) {
     					// Requires computation, but we're
     					// copying someone else's computation.
    

    staticcopy関数内で、ll = nod(OXXX, N, N);*ll = n1;の後に、ll->orig = ll;という行が追加されています。

  2. test/fixedbugs/issue8028.go:

    --- /dev/null
    +++ b/test/fixedbugs/issue8028.go
    @@ -0,0 +1,27 @@
    +// compile
    +
    +// Copyright 2014 The Go Authors.  All rights reserved.
    +// Use of this source code is governed by a BSD-style
    +// license that can be found in the LICENSE file.
    +
    +// Issue 8028. Used to fail in -race mode with "non-orig name" error.
    +
    +package p
    +
    +var (
    +	t2 = T{F, "s1"}
    +	t1 = T{F, "s2"}
    +
    +	tt = [...]T{t1, t2}
    +)
    +
    +type I interface{}
    +
    +type T struct {
    +	F func() I
    +	S string
    +}
    +
    +type E struct{}
    +
    +func F() I { return new(E) }
    

    Issue #8028を再現し、修正が正しく適用されたことを確認するための新しいテストファイルが追加されています。このテストは、-raceモードでコンパイルした際に「non-orig name」エラーが発生しないことを検証します。

コアとなるコードの解説

src/cmd/gc/sinit.cの変更点であるll->orig = ll;は、このコミットの核心です。

staticcopy関数は、静的初期化のコンテキストで、あるノードn1の内容を新しいノードllにコピーする役割を担っています。 元のコードでは、以下の手順でノードが作成・コピーされていました。

  1. ll = nod(OXXX, N, N);: 新しい空のノードllが作成されます。この時点では、ll->origは通常nilまたはデフォルト値です。
  2. *ll = n1;: n1の全てのフィールドがllにコピーされます。これにはn1->origも含まれます。もしn1が既に別のノードのコピーであった場合、ll->orign1->origと同じ値を持ち、ll自身を指しません。

この挙動が問題でした。コンパイラの解析フェーズ、特にレース検出器は、ノードのorigフィールドを辿って、そのノードが表す論理的なエンティティ(例えば、変数)の「真のオリジナル」に到達しようとします。もしlln1のコピーであり、n1がさらに別のノードのコピーである場合、llorigフィールドがn1origフィールドを指していると、解析は正しい「オリジナル」に到達できないか、あるいは誤ったノードに情報を記録してしまう可能性がありました。これにより、addrtakenのような重要なフラグが正しく伝播せず、レース検出器が誤った状態を報告し、「non-orig name」エラーが発生していました。

追加されたll->orig = ll;という行は、*ll = n1;によるコピーの後で、明示的にllorigフィールドをll自身に設定し直します。この操作により、lln1の構造(型、オペレーションなど)を継承しつつも、その「オリジナル」としてはll自身が設定されます。コミットメッセージにある「completely separate copy」というコメントが示す通り、lln1の論理的なコピーではなく、n1の構造を持つ新しい独立したノードとして扱われるようになります。

これにより、llに対して行われるaddrtakenなどの解析結果は、ll自身に正しく関連付けられ、他のノードのorigチェーンに影響を与えることなく、独立して処理されるようになります。結果として、レース検出器が期待する情報と実際のノードの状態との間の不整合が解消され、「non-orig name」エラーが修正されます。

test/fixedbugs/issue8028.goは、この修正を検証するためのテストケースです。このテストは、静的初期化される構造体配列ttを定義しており、その要素t1t2が関数ポインタFと文字列を持つT型の構造体です。F関数はnew(E)を返すため、ヒープ割り当てが発生し、レース検出器が監視する対象となります。この種のコードパターンが、修正前のコンパイラでは「non-orig name」エラーを引き起こしていました。このテストがエラーなくコンパイルできることで、修正が正しく機能していることが確認されます。

関連リンク

参考にした情報源リンク

  • Go言語の公式ドキュメントおよびソースコード
  • Goコンパイラの内部構造に関する一般的な知識
  • データ競合検出器に関する一般的な情報
  • Go言語のIssueトラッカーおよびコードレビューシステム (Gerrit)