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

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

コミット

commit 91b1f7cb15700f39ca63c4e056b41d9b04100e97
Author: Russ Cox <rsc@golang.org>
Date:   Thu Feb 13 22:45:16 2014 -0500

    cmd/gc: handle variable initialization by block move in liveness
    
    Any initialization of a variable by a block copy or block zeroing
    or by multiple assignments (componentwise copying or zeroing
    of a multiword variable) needs to emit a VARDEF. These cases were not.
    
    Fixes #7205.
    
    TBR=iant
    CC=golang-codereviews
    https://golang.org/cl/63650044

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

https://github.com/golang/go/commit/91b1f7cb15700f39ca63c4e056b41d9b04100e97

元コミット内容

cmd/gc: handle variable initialization by block move in liveness

Any initialization of a variable by a block copy or block zeroing
or by multiple assignments (componentwise copying or zeroing
of a multiword variable) needs to emit a VARDEF. These cases were not.

Fixes #7205.

TBR=iant
CC=golang-codereviews
https://golang.org/cl/63650044

変更の背景

このコミットは、Goコンパイラ(cmd/gc)における変数のライブネス解析の正確性を向上させることを目的としています。具体的には、ブロックコピー(block move)やブロックゼロ化(block zeroing)、あるいは複数回の代入(多ワード変数のコンポーネントごとのコピーやゼロ化)によって変数が初期化される場合に、コンパイラがVARDEF(Variable Definition)イベントを正しく発行していなかった問題に対処しています。

ライブネス解析は、プログラムの特定のポイントでどの変数が「ライブ」(将来使用される可能性がある)であるかを判断するコンパイラの最適化フェーズです。この解析は、レジスタ割り当て、デッドコード削除、スタックフレームの最適化など、多くの重要な最適化の基盤となります。ライブネス解析が正しく機能するためには、変数がいつ定義(初期化)され、いつ使用され、いつデッドになるかを正確に追跡する必要があります。

VARDEFは、コンパイラが変数の定義サイトをマークするために使用する内部的な命令またはイベントです。変数が初期化されたことをコンパイラに通知し、ライブネス解析がその変数のライフタイムを正確に開始できるようにします。以前の実装では、単純な単一の代入による初期化はVARDEFを生成していましたが、より複雑な初期化パターン(例えば、構造体全体のコピーや、複数のフィールドへの個別の代入による構造体の初期化)ではVARDEFが欠落していました。

この欠落により、ライブネス解析が変数を早期にデッドと判断したり、逆に不要な変数をライブと判断し続けたりする可能性がありました。これは、生成されるコードの効率性やデバッグ情報の正確性に悪影響を及ぼす可能性があります。コミットメッセージにあるFixes #7205は、この問題が特定のバグとして報告されていたことを示唆しています。

前提知識の解説

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

  1. Goコンパイラ (cmd/gc, 5g, 6g, 8g):

    • cmd/gcはGo言語の公式コンパイラです。Go 1.5以降は単一のgo tool compileコマンドに統合されましたが、このコミットが作成された2014年当時は、各アーキテクチャ(例: 5gはARM、6gはx86-64、8gはx86-32)ごとに異なるコンパイラバイナリが存在していました。これらのコンパイラは共通のフロントエンドと最適化パスを共有し、バックエンドでアーキテクチャ固有のコード生成を行っていました。
    • cgen.cファイルは、これらのアーキテクチャ固有のコンパイラにおけるコード生成(code generation)を担当する部分です。
    • pgen.cplive.cは、コンパイラの共通部分(プラットフォーム非依存)で、それぞれプログラミング言語の構造から中間表現への変換(pgen)とライブネス解析(plive)を担当します。
  2. ライブネス解析 (Liveness Analysis):

    • コンパイラのデータフロー解析の一種で、プログラムの各ポイントにおいて、どの変数の値が将来使用される可能性があるか(「ライブ」であるか)を決定します。
    • 変数がライブであるとは、その変数の現在の値がプログラムの実行パスのどこかで読み取られる可能性があることを意味します。
    • 変数がデッドであるとは、その変数の現在の値が将来使用されることがないことを意味します。
    • ライブネス情報は、レジスタ割り当て(ライブな変数をレジスタに保持し、デッドな変数のレジスタを再利用する)、デッドストア削除(デッドな変数への書き込みを削除する)、スタックフレームのサイズ決定などに利用されます。
  3. VARDEF:

    • Goコンパイラの内部的な概念で、変数が「定義された」(つまり、有効な値が割り当てられた)ことを示すマーカーです。
    • ライブネス解析は、VARDEFイベントを検出することで、変数のライブネスの開始点を認識します。これにより、変数のライフタイムを正確に追跡し、不要なレジスタ保持やメモリ割り当てを防ぎます。
  4. ブロックコピー (Block Copy) とブロックゼロ化 (Block Zeroing):

    • ブロックコピー: 構造体や配列のような複合型全体を、メモリ上で一括してコピーする操作です。例えば、dst = srcのような代入で、dstsrcが大きな構造体である場合、コンパイラはこれをバイト単位のブロックコピーとして実装することがあります。
    • ブロックゼロ化: 構造体や配列のような複合型のメモリ領域全体を、一括してゼロで埋める操作です。変数が初期化時にゼロ値で埋められる場合などに使用されます。
    • これらの操作は、複数のフィールドや要素にわたる初期化を効率的に行うためのものです。
  5. Goコンパイラにおけるノードの種類 (ONAME, PEXTERN, PPARAMOUT, PAUTO, PPARAM):

    • Goコンパイラは、プログラムのソースコードを抽象構文木(AST)として表現します。ASTの各要素は「ノード」と呼ばれます。
    • ONAME: 変数や関数などの名前を表すノードです。
    • PEXTERN: 外部リンケージを持つ変数(グローバル変数など)を表すクラスです。
    • PPARAMOUT: 関数の戻り値パラメータを表すクラスです。
    • PAUTO: ローカル変数(自動変数)を表すクラスです。
    • PPARAM: 関数の入力パラメータを表すクラスです。
    • これらのクラスは、変数のスコープやストレージクラスを区別するために使用されます。
  6. isfatisfunny 関数:

    • isfat(type): 型が「fat」(つまり、複数のワードを占める大きな型、例えば構造体や配列)であるかどうかをチェックするGoコンパイラ内部のヘルパー関数です。
    • isfunny(node): 特定の特殊なノード(例えば、.fp (フレームポインタ)、.args (関数引数領域)、_ (ブランク識別子))を識別するためのヘルパー関数です。これらのノードは通常の変数とは異なるライブネスの振る舞いをするため、特別に扱われることがあります。

技術的詳細

このコミットの核心は、Goコンパイラのコード生成フェーズ(sgen関数)とライブネス解析フェーズ(plive.c)におけるVARDEFイベントの生成と処理の改善にあります。

以前のGoコンパイラでは、変数の初期化が単純な単一の代入(例: x = 10)である場合、VARDEFが適切に発行されていました。しかし、以下のようなケースではVARDEFが欠落していました。

  1. ブロックコピーによる初期化:

    type S struct {
        a, b, c int
    }
    var s1 S
    var s2 S
    s2 = s1 // s2の初期化がブロックコピーで行われる
    

    この場合、s2全体がs1からコピーされることで初期化されますが、コンパイラはs2が「定義された」ことを示すVARDEFを適切に発行していませんでした。

  2. ブロックゼロ化による初期化:

    type S struct {
        a, b, c int
    }
    var s S // sはゼロ値で初期化される
    

    複合型がゼロ値で初期化される場合、そのメモリ領域はゼロで埋められます。この操作もVARDEFを必要としますが、発行されていませんでした。

  3. 複数回の代入による多ワード変数の初期化:

    type S struct {
        a, b int
    }
    var s S
    s.a = 1
    s.b = 2 // sの各コンポーネントが個別に初期化される
    

    多ワード変数(例: 構造体)が、そのコンポーネント(フィールド)ごとに個別の代入によって初期化される場合、全体としての変数の定義が正しく追跡されていませんでした。

これらのケースでは、sgen関数(Goコンパイラのコード生成器)が、変数の初期化のためにメモリ操作(ブロックコピーやゼロ化)を生成しますが、その際にVARDEFを伴っていませんでした。結果として、ライブネス解析はこれらの変数がいつ「ライブ」になったかを正確に判断できず、誤った最適化やデバッグ情報の不正確さにつながっていました。

このコミットでは、sgen関数内で、res(結果変数)がONAMEノードであり、かつ外部変数(PEXTERN)でない場合に、gvardef(res)を呼び出すように変更されました。これにより、ブロックコピーやゼロ化によって初期化される変数に対してもVARDEFが発行されるようになります。

また、関数の戻り値(.argsシンボルで表される)の初期化についても同様の修正が行われました。関数が複数の戻り値を返す場合、それらは.argsという特殊なシンボルを通じて扱われます。これらの戻り値パラメータ(PPARAMOUT)も、gvardefによって定義サイトがマークされるようになりました。

plive.cにおける変更は、ライブネス解析のロジックを調整し、VARDEFの新しい生成パターンに対応するためのものです。特に、isfunny関数からブランク識別子_が除外され、progeffects関数におけるVARDEFの処理がより汎用的に変更されました。これにより、VARDEFが発行された変数は、その定義サイトで正しくライブとしてマークされるようになります。

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

このコミットでは、主に以下のファイルが変更されています。

  1. src/cmd/5g/cgen.c, src/cmd/6g/cgen.c, src/cmd/8g/cgen.c:

    • 各アーキテクチャ(ARM, x86-64, x86-32)のコード生成器ファイル。
    • sgen関数内に、変数の定義サイトをライブネス解析のために記録するgvardef(res)の呼び出しが追加されました。
    • 特に、res->op == ONAME && res->class != PEXTERNの場合にgvardef(res)が呼ばれます。
    • また、.argsシンボルがコピーされる場合(関数の戻り値の初期化に相当)、その関数の戻り値パラメータ(PPARAMOUT)に対してもgvardefが呼ばれるようになりました。
  2. src/cmd/gc/pgen.c:

    • gvardef関数のエラーチェックが変更されました。以前はisfat(n->type)(ノードの型がfatであるか)をチェックしていましたが、これは削除され、単にn == N(ノードがnilでないか)をチェックするようになりました。これは、gvardefがfatでない変数に対しても呼ばれるようになったためです。
  3. src/cmd/gc/plive.c:

    • isfunny関数から、ブランク識別子_が除外されました。これにより、_が通常の変数と同様にライブネス解析の対象となる可能性が示唆されます(ただし、_は通常、値を破棄するために使用されるため、ライブネス解析の対象から外れることが多いですが、この変更はより一般的なVARDEFの処理を可能にします)。
    • progeffects関数内のライブネス解析ロジックが変更されました。特に、from->nodeto->nodeisfunnyでないことのチェックが削除され、より一般的な条件でライブネスの更新が行われるようになりました。
    • LeftWriteフラグがセットされている場合、isfat(from->node->type)のチェックが削除され、prog->as == AVARDEFの場合にのみvarkillがセットされるようになりました。
    • to->node->addrtakenの場合のavarinitvarkillの処理ロジックが変更され、AVARDEFの場合にvarkillがセットされるようになりました。
  4. test/live.go:

    • 新しいテストケースf8f9が追加されました。
    • f8はブロックリターン(return g8())がライブネス解析に与える影響をテストします。
    • f9はブロック代入(x := i9)がライブネス解析に与える影響をテストし、issue 7205に関連する問題の再現と修正の検証を目的としています。

コアとなるコードの解説

このコミットの主要な変更は、Goコンパイラのコード生成フェーズ(sgen)とライブネス解析フェーズ(plive.c)の連携を強化し、変数の初期化がより複雑な形式で行われる場合でも、ライブネス解析が正確に行われるようにすることです。

src/cmd/{5,6,8}g/cgen.c の変更

 // Record site of definition of ns for liveness analysis.
 if(res->op == ONAME && res->class != PEXTERN)
  gvardef(res);

 // If copying .args, that's all the results, so record definition sites
 // for them for the liveness analysis.
 if(res->op == ONAME && strcmp(res->sym->name, ".args") == 0)
  for(l = curfn->dcl; l != nil; l = l->next)
   if(l->n->class == PPARAMOUT)
    gvardef(l->n);
  • gvardef(res) の追加: sgen関数は、代入や初期化のコードを生成する役割を担っています。ここで、res(結果として値が代入されるノード)がONAME(名前付き変数)であり、かつPEXTERN(外部変数、つまりグローバル変数など)でない場合に、gvardef(res)が呼び出されます。これは、ローカル変数やパラメータがブロックコピーやゼロ化によって初期化される際に、その変数が「定義された」ことをライブネス解析に明示的に通知するためのものです。以前は、このような複雑な初期化ではVARDEFが発行されず、ライブネス解析が変数のライフタイムを誤って判断する可能性がありました。
  • .args の特殊処理: Go言語では、複数の戻り値を持つ関数は、内部的に.argsという特殊なシンボルを通じて戻り値の領域を扱います。このコードブロックは、.argsシンボルがコピーされる場合(これは関数の戻り値が初期化されることを意味します)、その関数のすべての戻り値パラメータ(PPARAMOUT)に対してgvardefを呼び出します。これにより、関数の戻り値もライブネス解析によって正しく追跡されるようになります。

src/cmd/gc/pgen.c の変更

 void
 gvardef(Node *n)
 {
- if(n == N || !isfat(n->type))
-  fatal("gvardef: node is not fat");
+ if(n == N)
+  fatal("gvardef nil");
  switch(n->class) {
  case PAUTO:
  case PPARAM:
  • gvardef のエラーチェックの緩和: 以前のgvardef関数は、引数nN(nil)でないことと、isfat(n->type)(ノードの型が「fat」、つまり複数ワードを占める大きな型であること)を前提としていました。このコミットでは、!isfat(n->type)のチェックが削除されました。これは、sgen関数からのgvardef呼び出しが、fatでない(単一ワードの)変数に対しても行われるようになったためです。これにより、gvardefはより汎用的な変数の定義マーカーとして機能するようになります。

src/cmd/gc/plive.c の変更

 static int
 isfunny(Node *node)
 {
- char *names[] = { ".fp", ".args", "_", nil };
+ char *names[] = { ".fp", ".args", nil };
  int i;

  if(node->sym != nil && node->sym->name != nil)
  • isfunny から _ の削除: isfunny関数は、ライブネス解析において特殊な振る舞いをするノード(例えば、フレームポインタや関数引数領域)を識別するために使用されていました。以前はブランク識別子_もこのリストに含まれていましたが、この変更により_がリストから削除されました。これは、_が通常の変数と同様にVARDEFによって定義サイトがマークされるようになったため、isfunnyによる特別な除外が不要になったことを示唆しています。
  if(info.flags & (LeftRead | LeftWrite | LeftAddr)) {
   from = &prog->from;
-  if (from->node != nil && !isfunny(from->node) && from->sym != nil) {
-   switch(prog->from.node->class & ~PHEAP) {
+  if (from->node != nil && from->sym != nil) {
+   switch(from->node->class & ~PHEAP) {
    case PAUTO:
    case PPARAM:
    case PPARAMOUT:
  if(info.flags & (RightRead | RightWrite | RightAddr)) {
   to = &prog->to;
-  if (to->node != nil && to->sym != nil && !isfunny(to->node)) {
-   switch(prog->to.node->class & ~PHEAP) {
+  if (to->node != nil && to->sym != nil) {
+   switch(to->node->class & ~PHEAP) {
    case PAUTO:
    case PPARAM:
    case PPARAMOUT:
  • isfunny チェックの削除: progeffects関数は、個々の命令(Prog)がライブネスに与える影響を計算します。以前は、命令のfromオペランドやtoオペランドがisfunnyである場合、ライブネス解析の対象から除外されていました。この変更により、!isfunny(from->node)!isfunny(to->node)のチェックが削除されました。これは、VARDEFの生成がより正確になったため、これらの特殊なノードに対してもライブネス解析がより適切に適用されるようになったことを意味します。
      if(info.flags & LeftWrite)
-       if(from->node != nil && (!isfat(from->node->type) || prog->as == AVARDEF))
+       if(from->node != nil && !isfat(from->node->type))
         bvset(varkill, pos);
  • LeftWrite 処理の変更: LeftWriteフラグは、命令が左オペランド(from)に書き込むことを示します。以前は、from->nodeがfatでない型であるか、または命令がAVARDEFVARDEF命令)である場合にvarkill(変数がデッドになるビットベクトル)がセットされていました。この変更により、prog->as == AVARDEFの条件が削除されました。これは、VARDEFが変数を定義する命令であり、その命令自体が変数を「デッド」にするわけではないため、この条件が不要になったことを示唆しています。
     if(to->node->addrtaken) {
-     //if(prog->as == AKILL)
-     // bvset(varkill, pos);
-     //else
-     bvset(avarinit, pos);
+     bvset(avarinit, pos);
+     if(prog->as == AVARDEF)
+      bvset(varkill, pos);
     } else {
      if(info.flags & (RightRead | RightAddr))
       bvset(uevar, pos);
  • addrtaken 変数の処理の改善: addrtakenフラグは、変数のアドレスが取得されたことを示します。アドレスが取得された変数は、ポインタを通じてアクセスされる可能性があるため、ライブネス解析で特別な注意が必要です。この変更では、addrtaken変数がtoオペランドである場合、avarinit(変数が初期化されたビットベクトル)が常にセットされるようになりました。さらに、命令がAVARDEFである場合にのみvarkillがセットされます。これは、VARDEFが変数を定義する際に、その変数の以前の値を「デッド」にする(つまり、新しい値で上書きする)ことを正確に反映するためのものです。

これらの変更により、Goコンパイラは、ブロックコピー、ブロックゼロ化、および複数回の代入による変数の初期化を、ライブネス解析の観点からより正確に処理できるようになりました。これにより、コンパイラが生成するコードの品質とデバッグ情報の正確性が向上します。

関連リンク

参考にした情報源リンク

  • Goコンパイラのソースコード (上記コミットのファイル群)
  • コンパイラ理論に関する一般的な知識 (ライブネス解析、ASTなど)
  • Go言語の内部構造に関する一般的な知識
  • Go issue 7205 (検索したが直接的な情報は見つからず、コミットメッセージとコードから推測)