[インデックス 19045] ファイルの概要
このコミットは、Goコンパイラのcmd/8g
(x86アーキテクチャ、特に387浮動小数点ユニットを使用するビルド)におけるライブネス解析のバグ修正に関するものです。具体的には、浮動小数点命令がメモリを書き込む際のライブネス情報が正しく伝達されない問題を解決しています。
影響を受けるファイルは以下の通りです。
src/cmd/8g/prog.c
: 命令のプロパティを定義するテーブルが含まれています。src/cmd/gc/plive.c
: ライブネス解析のロジックが含まれています。
コミット
commit 844ec6bbe3753c8142fe3b45cf288749ffa9493a
Author: Russ Cox <rsc@golang.org>
Date: Sun Apr 6 10:30:02 2014 -0400
cmd/8g: fix liveness for 387 build (including plan9)
TBR=khr
CC=golang-codereviews
https://golang.org/cl/84570045
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/844ec6bbe3753c8142fe3b45cf288749ffa9493a
元コミット内容
cmd/8g: fix liveness for 387 build (including plan9)
変更の背景
このコミットの背景には、Goコンパイラが生成するコードの最適化、特にレジスタ割り当てとライブネス解析における問題がありました。
Goコンパイラは、プログラムの実行中にどの変数が「生きている」(将来使用される可能性がある)かを判断するためにライブネス解析を行います。この情報は、レジスタ割り当て(頻繁に使用される変数をCPUレジスタに配置して高速アクセスを可能にする最適化)や、ガベージコレクション(不要になったメモリを解放するプロセス)において非常に重要です。
問題は、x86アーキテクチャの387浮動小数点ユニット(FPU)を使用する特定の命令(AFMOVDP
, AFMOVFP
など)のライブネス情報が正しく処理されていなかったことにありました。これらの命令はメモリから値を読み込み(LeftRead
)、そのアドレスを操作する(RightAddr
)と同時に、そのメモリ位置に値を書き込む可能性がありました。
従来のProgInfo
フラグの定義では、RightAddr
が設定されている場合、それは暗黙的にメモリ位置からの読み込みを意味すると解釈されていました。しかし、FPU命令の中には、アドレスを「取得」するものの、そのアドレスの既存の値を「読み込む」のではなく、単にそのアドレスに「書き込む」だけのものがありました。この誤解釈により、ライブネス解析が、実際には不要になったメモリ位置を「生きている」と誤って判断し、レジスタ割り当ての非効率性や、場合によっては誤ったコード生成につながる可能性がありました。
特に、レジスタ最適化器はRightAddr
フラグを見て、メモリ参照をレジスタ参照に置き換えようとしますが、これはFPU命令がメモリに直接書き込む場合に問題となります。このコミットは、この特定のケースを正しく扱うことで、ライブネス解析の精度を向上させ、より正確なレジスタ割り当てとコード生成を可能にすることを目的としています。
前提知識の解説
Goコンパイラの構造 (gc, 8g)
Goコンパイラは、主に以下の部分から構成されます。
gc
(Go Compiler): Go言語のソースコードを中間表現に変換し、最適化を行い、最終的にアセンブリコードを生成する主要なコンポーネントです。プラットフォームに依存しない最適化や型チェックなどを行います。cmd/8g
:gc
によって生成された中間表現を受け取り、x86アーキテクチャ(特に32ビット版)向けのアセンブリコードを生成するバックエンドコンパイラです。8g
の8
はx86アーキテクチャを指します。同様に、6g
はamd64、5g
はARMなど、異なるアーキテクチャに対応するバックエンドが存在します。
ライブネス解析 (Liveness Analysis)
ライブネス解析は、コンパイラのデータフロー解析の一種で、プログラムの特定のポイントにおいて、どの変数が「生きている」か(その変数の値が将来の計算で使われる可能性があるか)を判断します。
- 「生きている」変数: その変数の現在の値が、プログラムの実行パスのどこかで後続の命令によって読み取られる可能性がある場合。
- 「死んでいる」変数: その変数の現在の値が、それ以降のどの命令によっても読み取られることがない場合。
ライブネス情報は、レジスタ割り当てにおいて非常に重要です。死んでいる変数が占有しているレジスタは、他の生きている変数に再利用できます。また、ガベージコレクションでは、生きているオブジェクトのみが保持され、死んでいるオブジェクトは解放されます。
レジスタ割り当て (Register Allocation)
レジスタ割り当ては、コンパイラの最適化フェーズの一つで、プログラムの変数をCPUの高速なレジスタに割り当てるプロセスです。レジスタはメモリよりもはるかに高速にアクセスできるため、適切にレジスタを使用することでプログラムの実行速度を大幅に向上させることができます。
レジスタ割り当て器は、ライブネス解析の結果を利用して、どの変数をどのレジスタに割り当てるか、いつレジスタを解放して他の変数に再利用するかを決定します。
x86 (387) アーキテクチャの浮動小数点命令 (FPU instructions)
x86アーキテクチャには、浮動小数点演算を専門に行う浮動小数点ユニット(FPU)があります。初期のx86プロセッサでは、FPUは独立したコプロセッサ(Intel 80387など)として存在し、後にCPUに統合されました。
387 FPUは、スタックベースのレジスタセット(ST(0)からST(7))を使用して浮動小数点演算を行います。メモリとFPUレジスタ間でデータを移動するための特別な命令セットがあります。
FMOV
系の命令: 浮動小数点値をメモリとFPUレジスタ間で移動させる命令です。例えば、FMOVD
は倍精度浮動小数点数を扱います。
prog.c
と plive.c
の役割
src/cmd/8g/prog.c
: このファイルには、Goコンパイラが生成する各アセンブリ命令(Prog
構造体)に関するメタデータが定義されています。このメタデータはProgInfo
構造体として表現され、命令がオペランドをどのように使用するか(読み込み、書き込み、アドレスの取得など)を示すフラグが含まれています。レジスタ割り当て器やライブネス解析器は、この情報に基づいて動作します。src/cmd/gc/plive.c
: このファイルには、Goコンパイラのライブネス解析の主要なロジックが実装されています。prog.c
で定義された命令のプロパティ(ProgInfo
フラグ)を参照し、プログラムの各ポイントでの変数のライブネスを計算します。
ProgInfo
とフラグ (LeftAddr
, RightWrite
, RightRead
, RightAddr
)
ProgInfo
構造体は、各命令のオペランドの使用方法を記述するビットフラグの集合です。
LeftAddr
: 命令の左オペランドがアドレスとして使用されることを示します。RightWrite
: 命令の右オペランドが書き込み先として使用されることを示します。つまり、そのメモリ位置の既存の値は上書きされ、読み込まれません。RightRead
: 命令の右オペランドが読み込み元として使用されることを示します。RightAddr
: 命令の右オペランドがアドレスとして使用されることを示します。
これらのフラグは、コンパイラが命令のセマンティクスを理解し、ライブネス解析やレジスタ割り当てを正しく実行するために不可欠です。
技術的詳細
このコミットの核心は、ProgInfo
フラグの組み合わせがライブネス解析に与える影響を修正することにあります。
以前のplive.c
のライブネス解析ロジックでは、info.flags & (RightRead | RightAddr)
という条件がありました。これは、「右オペランドが読み込みであるか、または右オペランドがアドレスである場合」に、そのメモリ位置が「生きている」と判断するという意味でした。
しかし、387 FPUのFMOV
命令群(AFMOVDP
, AFMOVFP
など)は、メモリのアドレスを右オペランドとして受け取ります(RightAddr
)。これらの命令は、そのアドレスに浮動小数点値を「書き込む」ことが主な目的であり、そのアドレスの既存の値を「読み込む」わけではありません。
問題は、RightAddr
フラグが単独で設定されている場合、ライブネス解析器がこれを「暗黙的な読み込み」と解釈してしまっていた点にあります。これにより、実際には上書きされるだけで読み込まれないメモリ位置が「生きている」と誤って判断され、レジスタ割り当て器がそのメモリ位置をレジスタに割り当てようとしたり、不要なレジスタを保持し続けたりする可能性がありました。
この修正では、prog.c
のprogtable
において、これらのFPU命令に対して新たにRightWrite
フラグを追加しました。これにより、命令がアドレスを操作しつつも、そのアドレスに書き込むことを明示的に示します。
そして、plive.c
のライブネス解析ロジックを以下のように変更しました。
if((info.flags & RightRead) || (info.flags & (RightAddr|RightWrite)) == RightAddr)
この新しい条件は、以下のいずれかの条件が満たされた場合に、メモリ位置が「生きている」と判断します。
info.flags & RightRead
: 右オペランドが明示的に読み込みである場合。これは以前と同じです。(info.flags & (RightAddr|RightWrite)) == RightAddr
: 右オペランドがアドレスであり、かつRightWrite
フラグが設定されていない場合。つまり、RightAddr
が単独で設定されている場合にのみ、暗黙的な読み込みと見なします。もしRightAddr
とRightWrite
が両方設定されている場合は、これは単なる書き込みであり、読み込みではないと判断されます。
この変更により、FPU命令がメモリに書き込む際に、そのメモリ位置が不必要に「生きている」と判断されることがなくなり、ライブネス解析の精度が向上しました。結果として、レジスタ割り当て器はより効率的に動作し、生成されるコードの品質が向上します。
また、コメントで述べられているように、RightAddr
フラグは、レジスタ最適化器がメモリ参照をレジスタに置き換えようとするのを防ぐ役割も果たします。これは、FPU命令がメモリに直接アクセスする必要があるため重要です。RightAddr|RightWrite
の組み合わせは、アドレスが取得されるが、その目的は書き込みのみであり、レジスタ化すべきではないことを明確に伝えます。
コアとなるコードの変更箇所
src/cmd/8g/prog.c
の変更
--- a/src/cmd/8g/prog.c
+++ b/src/cmd/8g/prog.c
@@ -138,11 +138,16 @@ static ProgInfo progtable[ALAST] = {
[AFMOVW]=\t{SizeW | LeftAddr | RightWrite},\n
[AFMOVV]=\t{SizeQ | LeftAddr | RightWrite},\n
-\t[AFMOVDP]=\t{SizeD | LeftRead | RightAddr},\n
-\t[AFMOVFP]=\t{SizeF | LeftRead | RightAddr},\n
-\t[AFMOVLP]=\t{SizeL | LeftRead | RightAddr},\n
-\t[AFMOVWP]=\t{SizeW | LeftRead | RightAddr},\n
-\t[AFMOVVP]=\t{SizeQ | LeftRead | RightAddr},\n
+\t// These instructions are marked as RightAddr
+\t// so that the register optimizer does not try to replace the
+\t// memory references with integer register references.\n
+\t// But they do not use the previous value at the address, so
+\t// we also mark them RightWrite.\n
+\t[AFMOVDP]=\t{SizeD | LeftRead | RightWrite | RightAddr},\n
+\t[AFMOVFP]=\t{SizeF | LeftRead | RightWrite | RightAddr},\n
+\t[AFMOVLP]=\t{SizeL | LeftRead | RightWrite | RightAddr},\n
+\t[AFMOVWP]=\t{SizeW | LeftRead | RightWrite | RightAddr},\n
+\t[AFMOVVP]=\t{SizeQ | LeftRead | RightWrite | RightAddr},\n
[AFMULD]=\t{SizeD | LeftAddr | RightRdwr},\n
[AFMULDP]=\t{SizeD | LeftAddr | RightRdwr},\n
src/cmd/gc/plive.c
の変更
--- a/src/cmd/gc/plive.c
+++ b/src/cmd/gc/plive.c
@@ -755,7 +755,15 @@ Next:\n
if(prog->as == AVARDEF || prog->as == AVARKILL)\n
bvset(varkill, pos);\n
} else {\n
-\t\t\t\t\tif(info.flags & (RightRead | RightAddr))\n
+\t\t\t\t\t// RightRead is a read, obviously.\n
+\t\t\t\t\t// RightAddr by itself is also implicitly a read.\n
+\t\t\t\t\t//\n
+\t\t\t\t\t// RightAddr|RightWrite means that the address is being taken\n
+\t\t\t\t\t// but only so that the instruction can write to the value.\n
+\t\t\t\t\t// It is not a read. It is equivalent to RightWrite except that\n
+\t\t\t\t\t// having the RightAddr bit set keeps the registerizer from\n
+\t\t\t\t\t// trying to substitute a register for the memory location.\n
+\t\t\t\t\tif((info.flags & RightRead) || (info.flags & (RightAddr|RightWrite)) == RightAddr)\n
bvset(uevar, pos);\n
if(info.flags & RightWrite)\n
if(to->node != nil && (!isfat(to->node->type) || prog->as == AVARDEF))\n
コアとなるコードの解説
src/cmd/8g/prog.c
の変更点
prog.c
では、progtable
という配列が定義されており、各アセンブリ命令(ALAST
は命令の最大数)に対応するProgInfo
構造体を格納しています。このProgInfo
は、命令のオペランドがどのように使用されるかを示すビットフラグの集合です。
変更されたのは、387 FPUの浮動小数点移動命令であるAFMOVDP
, AFMOVFP
, AFMOVLP
, AFMOVWP
, AFMOVVP
のエントリです。
-
変更前:
[AFMOVDP]= {SizeD | LeftRead | RightAddr}, // ... 他のAFMOV*命令も同様
これらの命令は、左オペランドが読み込み(
LeftRead
)であり、右オペランドがアドレス(RightAddr
)であることを示していました。しかし、この設定では、ライブネス解析器がRightAddr
を「暗黙的な読み込み」と解釈してしまう問題がありました。 -
変更後:
[AFMOVDP]= {SizeD | LeftRead | RightWrite | RightAddr}, // ... 他のAFMOV*命令も同様
新たに
RightWrite
フラグが追加されました。これにより、これらの命令が右オペランドのアドレスに「書き込む」ことを明示的に示します。コメントにもあるように、RightAddr
はレジスタ最適化器がメモリ参照をレジスタに置き換えようとするのを防ぐために設定されていますが、RightWrite
を追加することで、そのアドレスの既存の値は使用されず、単に上書きされるだけであることをライブネス解析器に伝えます。
src/cmd/gc/plive.c
の変更点
plive.c
では、ライブネス解析の主要なロジックが実装されています。特に、Next
ラベルの後のブロックで、命令のオペランドが「使用される」(uevar
、use-expression variable)かどうかを判断する部分が変更されました。
-
変更前:
if(info.flags & (RightRead | RightAddr)) bvset(uevar, pos);
この条件は、「命令の
ProgInfo
フラグにRightRead
またはRightAddr
が含まれている場合、その右オペランドのメモリ位置は使用される(生きている)と見なす」というものでした。前述の通り、RightAddr
が単独で設定されている場合でも、これが暗黙的な読み込みと解釈されてしまう問題がありました。 -
変更後:
if((info.flags & RightRead) || (info.flags & (RightAddr|RightWrite)) == RightAddr) bvset(uevar, pos);
この新しい条件は、より正確なライブネス解析を可能にします。
info.flags & RightRead
: これは以前と同じで、右オペランドが明示的に読み込みである場合は、そのメモリ位置が使用されると判断します。(info.flags & (RightAddr|RightWrite)) == RightAddr
: この部分が重要です。これは、info.flags
がRightAddr
を含んでいるが、同時にRightWrite
を含んでいない場合にのみ真となります。つまり、右オペランドがアドレスとして使用されるが、そのアドレスへの書き込みは行われない(したがって、既存の値が読み込まれる可能性がある)場合にのみ、そのメモリ位置が使用されると判断します。- もし
info.flags
がRightAddr
とRightWrite
の両方を含んでいる場合(AFMOV*P
命令の新しい設定のように)、(RightAddr|RightWrite)
は両方のビットがセットされた値になり、== RightAddr
の条件は偽となります。これにより、書き込み専用のアドレス参照が誤って読み込みと解釈されるのを防ぎます。
- もし
この変更により、AFMOV*P
のような命令がメモリに書き込む際に、そのメモリ位置が不必要に「生きている」と判断されることがなくなり、ライブネス解析の精度が向上し、結果としてレジスタ割り当ての効率が改善されます。
関連リンク
- Go Code Review: https://golang.org/cl/84570045
参考にした情報源リンク
- Go言語のコンパイラに関する一般的な情報 (Go compiler internals, liveness analysis, register allocation)
- x86アーキテクチャとFPU命令に関する一般的な情報
- Go言語のソースコード(特に
src/cmd/gc
とsrc/cmd/8g
ディレクトリ) - https://github.com/golang/go/commit/844ec6bbe3753c8142fe3b45cf288749ffa9493a (コミットページ)
- https://golang.org/cl/84570045 (Go Code Review)
- https://go.dev/src/cmd/8g/prog.c (Goソースコード - prog.c)
- https://go.dev/src/cmd/gc/plive.c (Goソースコード - plive.c)