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

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

このコミットは、Go言語の初期のコンパイラである6g(x86-64アーキテクチャ向け)における構造体の幅の計算と、メモリコピーの最適化に関する修正を含んでいます。具体的には、関数の引数や戻り値として使用される構造体の幅の扱いを修正し、4バイトのメモリコピーにおいてより効率的なMOVSL命令を使用するように変更しています。

コミット

commit f61639d4e2657267b25fe6e867048f8bdd14f7ae
Author: Russ Cox <rsc@golang.org>
Date:   Mon Feb 2 13:41:38 2009 -0800

    6g return struct fix:
    make t->width of funarg struct be width of struct.
    
    emit MOVSL for 4-byte copy.
    
    R=ken
    OCL=24108
    CL=24111
---
 src/cmd/6g/align.c | 7 ++++++-\n src/cmd/6g/cgen.c  | 8 +++-----\n src/cmd/6g/gsubr.c | 4 +---\n 3 files changed, 10 insertions(+), 9 deletions(-)

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

https://github.com/golang/go/commit/f61639d4e2657267b25fe6e867048f8bdd14f7ae

元コミット内容

このコミットは、Goコンパイラ6gにおける以下の2つの主要な修正を含んでいます。

  1. 構造体の戻り値の幅の修正: 関数の引数または戻り値として扱われる構造体(funarg struct)の型情報におけるt->widthの計算方法を修正し、構造体本来の幅を正しく反映するようにしました。
  2. 4バイトコピーの最適化: メモリコピーのコード生成において、4バイトのコピーを行う際に、より効率的なMOVSL(Move String Doubleword)命令を使用するように変更しました。

変更の背景

このコミットが行われた2009年2月は、Go言語がまだ一般に公開される前の開発初期段階にあたります。当時のGoコンパイラは、各アーキテクチャ(例: x86-64向けは6g、x86向けは8g)ごとに独立した実装を持っていました。このような初期のコンパイラ開発では、型システムの正確な表現、特にメモリレイアウトやアライメントに関するバグが頻繁に発生します。

  1. 構造体の幅の計算の不正確さ: コンパイラが型情報を扱う際、各型のメモリ上でのサイズ(幅)を正確に把握することは極めて重要です。特に構造体の場合、そのメンバーの配置やアライメントによって全体のサイズが決定されます。関数の引数や戻り値として構造体が渡される場合、スタック上での領域確保やレジスタへの配置にこの幅情報が利用されます。もし幅の計算が間違っていると、メモリの不正アクセス、スタックの破損、あるいは間違った値の読み書きといった深刻なバグにつながる可能性があります。この修正は、funarg struct(関数引数/戻り値の構造体)のt->widthが正しく計算されていなかった問題を解決し、コンパイラの型システムの一貫性と正確性を向上させることを目的としています。

  2. メモリコピーの非効率性: コンパイラが生成するアセンブリコードの効率は、プログラム全体のパフォーマンスに直結します。特に、構造体のコピーや配列の操作など、頻繁に発生するメモリコピー処理は、その最適化が重要です。従来の6gコンパイラでは、4バイト以上のメモリコピーに対してREP MOVSB(Repeat Move String Byte)命令を使用していました。これはバイト単位で繰り返しコピーを行う命令ですが、x86-64アーキテクチャでは、より大きな単位(ワード、ダブルワード、クアッドワード)でコピーを行う命令が存在し、これらを利用する方が一般的に高速です。このコミットは、4バイトのコピーに対してMOVSL(Move String Doubleword)命令を導入することで、メモリコピーの効率を改善し、生成されるコードのパフォーマンスを向上させることを目指しました。

これらの修正は、Goコンパイラの安定性とパフォーマンスを初期段階から高めるための、基盤的な改善の一環として行われました。

前提知識の解説

このコミットを理解するためには、以下の知識が役立ちます。

  1. Goコンパイラと6g:

    • Go言語の初期のコンパイラは、ターゲットアーキテクチャごとに異なる名前を持っていました。6gはx86-64(AMD64)アーキテクチャ向けのGoコンパイラを指します。同様に、8gはx86(32ビット)向け、5gはARM向けなどがありました。これらはGoのソースコードを対応するアーキテクチャの機械語に変換する役割を担っていました。
    • 現在のGoコンパイラは、より統合されたツールチェインの一部として提供されており、これらのアーキテクチャ固有の命名は使われていません。
  2. 型システムとType構造体、width:

    • コンパイラ内部では、プログラム中のすべての変数、関数、リテラルなどが「型」として表現されます。Goコンパイラでは、これらの型情報を管理するためにTypeのような内部構造体を使用します。
    • Type構造体には、その型がメモリ上で占めるサイズ(バイト単位)を示すフィールドが含まれることが一般的です。このフィールドはしばしばwidthsizeといった名前で呼ばれます。
    • 構造体の場合、widthはメンバーの型とアライメント規則に基づいて計算されます。アライメントとは、メモリ上の特定のアドレス境界にデータを配置する規則であり、CPUが効率的にデータにアクセスするために重要です。
  3. funarg struct:

    • Go言語では、関数に構造体を引数として渡したり、構造体を戻り値として返したりすることができます。
    • コンパイラは、これらの構造体をスタックやレジスタを通じてどのように渡すか、あるいは返すかを決定します。この文脈で「funarg struct」は、関数の引数または戻り値として扱われる構造体の内部表現を指していると考えられます。これらの構造体のメモリレイアウトやサイズは、関数呼び出し規約に厳密に従う必要があります。
  4. x86/x86-64アセンブリ命令(メモリコピー関連):

    • MOVSB (Move String Byte): DS:SI(またはRSI)が指すメモリ位置から1バイトをES:DI(またはRDI)が指すメモリ位置にコピーし、SIDIをインクリメント(またはデクリメント)します。
    • MOVSL (Move String Doubleword): DS:SI(またはRSI)が指すメモリ位置から4バイト(ダブルワード)をES:DI(またはRDI)が指すメモリ位置にコピーし、SIDIを4だけインクリメント(またはデクリメント)します。
    • REP (Repeat Prefix): MOVSB, MOVSW, MOVSLなどの文字列操作命令の前に置かれるプレフィックスです。CX(またはRCX)レジスタに格納された回数だけ、その命令を繰り返します。例えば、REP MOVSBCXバイトをコピーします。
    • 効率性: 一般的に、CPUはより大きな単位(ワード、ダブルワード、クアッドワード)でメモリを操作する方が、バイト単位で操作するよりも効率的です。特に、データがアライメントされている場合、MOVSLのような命令はREP MOVSBよりも高速なコピーを実現できます。REPプレフィックスはループのオーバーヘッドをCPU内部で最適化しますが、短いコピーでは単一の大きな単位の命令の方が有利な場合があります。

技術的詳細

このコミットは、Goコンパイラのバックエンドにおける型情報の管理とコード生成の2つの側面を改善しています。

1. src/cmd/6g/align.c における構造体幅の計算修正

align.cは、Goの型がメモリ上でどのようにアライメントされ、どれくらいの幅を持つかを計算する役割を担っています。widstruct関数は、構造体の最終的な幅を決定する主要な関数です。

変更前:

t->width = o;

ここでは、o(おそらく現在のオフセットまたは計算された幅)がそのまま構造体twidthとして設定されていました。これは、構造体全体のサイズを単純に表しているように見えます。

変更後:

// type width only includes back to first field's offset
if(t->type == T)
    t->width = 0;
else
    t->width = o - t->type->width;

この変更は、t->widthの計算ロジックをより洗練されたものにしています。

  • t->type == Tという条件は、Tが何らかの特殊な型(例えば、構造体の内部表現における「開始」を示すマーカーのようなもの)を意味している可能性があります。この場合、幅を0に設定しています。
  • elseブロックでは、t->width = o - t->type->width;と計算されています。これは、oが構造体の現在の計算されたオフセットまたは全体の幅を示し、t->type->widthが構造体の「基底型」または「最初のフィールドまでの幅」を示していると推測されます。この差分を取ることで、t->widthが構造体内の特定のセクション、あるいは「最初のフィールドからの相対的な幅」を正確に表すように意図されていると考えられます。
  • コミットメッセージの「make t->width of funarg struct be width of struct」という記述から、この修正は特に、関数の引数や戻り値として扱われる構造体(funarg struct)のwidthが、その構造体本来のメモリサイズを正しく反映するようにするためのものであると理解できます。これにより、関数呼び出し時のスタックフレームのレイアウトやレジスタへの値のロードが正確に行われるようになります。

2. src/cmd/6g/cgen.c におけるメモリコピーの最適化

cgen.cは、Goのコードからアセンブリコードを生成する役割を担っています。sgen関数は、構造体や配列などのメモリブロックをコピーするコードを生成する部分です。

変更前:

if(c >= 4) {
    gconreg(AMOVQ, c, D_CX); // cバイトをCXレジスタにロード
    gins(AREP, N, N);        // REPプレフィックスを生成
    gins(AMOVSB, N, N);      // MOVSB命令を生成 (REP MOVSBでcバイトコピー)
} else
    // c < 4 の場合は、バイト単位のコピーを個別に生成

このコードは、コピーするバイト数cが4以上の場合に、REP MOVSB命令を使用してメモリコピーを行っていました。REP MOVSBは、CXレジスタに指定された回数だけバイト単位のコピーを繰り返す命令です。

変更後:

if(c >= 4) {
    gins(AMOVSL, N, N);    // MOVSL命令を生成 (4バイトコピー)
    c -= 4;                // コピーした4バイト分をcから減算
}
while(c > 0) {
    gins(AMOVSB, N, N);    // 残りのバイトをMOVSBでコピー
    c--;
}

この変更は、4バイト以上のコピーに対して、まずMOVSL命令を1回発行し、4バイトをコピーします。その後、残りのバイト数cが0になるまでMOVSB命令を繰り返し発行します。

  • MOVSLは4バイト(ダブルワード)を一度にコピーするため、REP MOVSBで4回バイトコピーを行うよりも効率的です。特に、コピーサイズが小さい(例: 4〜7バイト)場合、REPプレフィックスのオーバーヘッドを避けて単一のMOVSL命令を使用する方が高速になることがあります。
  • この最適化は、コンパイラが生成するコードの実行速度を向上させ、特に構造体のコピーが頻繁に行われるようなGoプログラムのパフォーマンスに寄与します。

3. src/cmd/6g/gsubr.c における引数幅の取得簡素化

gsubr.cは、Goコンパイラのバックエンドにおける汎用的なサブルーチンを含んでいます。setmaxarg関数は、関数の引数領域の最大幅を追跡するために使用されます。

変更前:

Type *to;
int32 w;

to = *getoutarg(t); // tから出力引数型を取得
w = to->width;       // その幅を取得

ここでは、getoutargという関数を呼び出して、関数の出力引数(戻り値)の型を取得し、その型のwidthを使用していました。

変更後:

int32 w;

w = t->argwid; // tのargwidフィールドを直接使用

この変更は、getoutargの呼び出しを削除し、t->argwidというフィールドを直接使用するように簡素化しています。

  • これは、align.cでのt->widthの計算修正と関連している可能性が高いです。align.cでの修正により、t(おそらく関数型)のargwidフィールドが、関数の引数/戻り値の幅を正確に保持するようになったため、getoutargのような補助関数を介さずに直接その値を利用できるようになったと考えられます。
  • この変更は、コードの簡素化と、コンパイラ内部での型情報のより直接的な利用を促進します。

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

src/cmd/6g/align.c

--- a/src/cmd/6g/align.c
+++ b/src/cmd/6g/align.c
@@ -68,7 +68,12 @@ widstruct(Type *t, uint32 o, int flag)
 	// final width is rounded
 	if(flag)
 		o = rnd(o, maxround);
-	t->width = o;
+
+	// type width only includes back to first field's offset
+	if(t->type == T)
+		t->width = 0;
+	else
+		t->width = o - t->type->width;
 	return o;
 }

src/cmd/6g/cgen.c

--- a/src/cmd/6g/cgen.c
+++ b/src/cmd/6g/cgen.c
@@ -906,11 +906,9 @@ sgen(Node *n, Node *ns, int32 w)
 		}
 
 		if(c >= 4) {
-\t\t\tgconreg(AMOVQ, c, D_CX);
-\t\t\tgins(AREP, N, N);\t// repeat
-\t\t\tgins(AMOVSB, N, N);\t// MOVB *(SI)+,*(DI)+
-\n-\t\t} else
+\t\t\tgins(AMOVSL, N, N);\t// MOVL *(SI)+,*(DI)+
+\t\t\tc -= 4;
+\t\t}
 		while(c > 0) {
 			gins(AMOVSB, N, N);\t// MOVB *(SI)+,*(DI)+
 			c--;

src/cmd/6g/gsubr.c

--- a/src/cmd/6g/gsubr.c
+++ b/src/cmd/6g/gsubr.c
@@ -1887,11 +1887,9 @@ lsort(Sig *l, int(*f)(Sig*, Sig*))
 void
 setmaxarg(Type *t)
 {
-\tType *to;
 \tint32 w;
 
-\tto = *getoutarg(t);
-\tw = to->width;
+\tw = t->argwid;
 	if(w > maxarg)
 		maxarg = w;
 }

コアとなるコードの解説

src/cmd/6g/align.c の変更

widstruct関数は、構造体のメモリレイアウトを計算し、その幅(t->width)を設定します。 変更前は、計算された最終的なオフセットoをそのままt->widthに代入していました。 変更後は、t->widthの計算に条件分岐が追加されました。

  • if(t->type == T): TはGoコンパイラ内部で使われる特殊な型識別子である可能性が高いです。この条件が真の場合、t->widthは0に設定されます。これは、特定のコンテキストで構造体の幅を0として扱う必要がある場合(例えば、構造体の開始点を示すためなど)に対応していると考えられます。
  • else: t->width = o - t->type->width;という計算が行われます。これは、oが構造体全体の計算された幅または現在のオフセットを示し、t->type->widthが構造体の基底型または最初のフィールドまでの幅を示していると解釈できます。この差分を取ることで、t->widthが構造体内のデータ部分の実際の幅、あるいは特定のオフセットからの相対的な幅を正確に表すように修正されています。これにより、特にfunarg struct(関数引数/戻り値の構造体)の幅が正しく計算され、関数呼び出し規約に沿ったメモリ管理が可能になります。

src/cmd/6g/cgen.c の変更

sgen関数は、メモリコピーのアセンブリコードを生成します。 変更前は、コピーサイズcが4バイト以上の場合にREP MOVSB命令(バイト単位の繰り返しコピー)を使用していました。これは、cの値をCXレジスタにロードし、REPプレフィックスとMOVSB命令を組み合わせることで、c回バイトコピーを実行するものです。 変更後は、cが4バイト以上の場合に、まずMOVSL命令(4バイト単位のコピー)を1回発行し、cから4を減算します。その後、残りのcが0になるまでMOVSB命令(バイト単位のコピー)を繰り返し発行します。 この変更により、4バイト以上のコピーにおいて、少なくとも最初の4バイトはより効率的なMOVSL命令で処理されるようになります。これにより、特にコピーサイズが小さい(例: 4〜7バイト)場合にREPプレフィックスのオーバーヘッドを回避し、全体的なコピー性能が向上します。

src/cmd/6g/gsubr.c の変更

setmaxarg関数は、関数の引数領域の最大幅を設定します。 変更前は、getoutarg(t)という関数を呼び出して、関数の出力引数(戻り値)の型を取得し、その型のwidthフィールドから幅wを取得していました。 変更後は、t->argwidというフィールドを直接参照して幅wを取得するように簡素化されています。 この変更は、align.cでのt->widthの計算修正と密接に関連しています。align.cの修正により、関数型targwidフィールドが、関数の引数/戻り値の幅を正確に保持するようになったため、getoutargのような間接的な呼び出しが不要になり、コードがより直接的で効率的になりました。

関連リンク

参考にした情報源リンク

  • Go言語の公式Gitリポジトリ: https://github.com/golang/go
  • Intel 64 and IA-32 Architectures Software Developer’s Manuals (x86アセンブリ命令の詳細確認のため)
  • Go言語の初期のコンパイラ設計に関する議論やドキュメント (当時のコンパイラの設計思想や型システムの理解のため)
  • 一般的なコンパイラ設計の原則と型システムに関する知識