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

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

このコミットは、Go言語のツールチェインに含まれるCコンパイラ cmd/6c (AMD64アーキテクチャ向け) および cmd/8c (x86アーキテクチャ向け) におけるスタックフレームのサイズ計算ロジックのバグ修正に関するものです。具体的には、ローカル変数(automatic)のスタック上でのアラインメントとサイズ計算が非効率的であった点を改善し、スタックフレームのサイズを約半分に削減します。

コミット

commit 139448fe95402d1b7ff0fa08c459a07de95ddbe4
Author: Russ Cox <rsc@golang.org>
Date:   Mon Feb 18 13:24:04 2013 -0500

    cmd/6c, cmd/8c: cut stack frames by about half
    
    The routine that adds an automatic to the stack was
    adding ptrsize-1 to the size before rounding up.
    That addition would only make sense to turn a round down
    into a round up. Before a round up, it just wastes a word.
    
    The effect was that a 6c function with one local and
    one two-word function call used (8+8)+(16+8) = 40 bytes
    instead of 8+16 = 24 bytes.
    
    The wasted space mostly didn't matter, but one place where
    it does matter is when trying to stay within the 128-byte
    total frame constraint for #pragma textflag 7 functions.
    
    This only affects the C compilers, not the Go compilers.
    
    5c already had correct code, which is now copied to 6c and 8c.
    
    R=ken2
    CC=golang-dev
    https://golang.org/cl/7303099

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

https://github.com/golang/go/commit/139448fe95402d1b7ff0fa08c459a07de95ddbe4

元コミット内容

このコミットは、cmd/6c および cmd/8c コンパイラがスタックフレームを構築する際に、ローカル変数(automatic)のサイズ計算に誤りがあったことを修正します。具体的には、スタックにローカル変数を追加するルーチンが、アラインメントのために切り上げを行う前に ptrsize-1 (ポインタサイズ-1) をサイズに加算していました。この ptrsize-1 の加算は、切り捨てを切り上げに変換する場合にのみ意味がありますが、切り上げの前にこれを行うと、無駄なワード(メモリ単位)を消費していました。

この結果、例えば 6c でコンパイルされた関数が1つのローカル変数と1つの2ワードの関数呼び出しを持つ場合、本来24バイトで済むところが40バイトものスタック領域を消費していました。この無駄なスペースは通常は大きな問題になりませんが、特に #pragma textflag 7 が指定された関数(Goランタイムの特定の最適化されたアセンブリ関数などで使用される、スタックフレームサイズが128バイトに制限されるケース)においては、この制約を超過する原因となっていました。

この修正はCコンパイラのみに影響し、Goコンパイラには影響しません。また、5c (ARMアーキテクチャ向けCコンパイラ) には既に正しいコードが実装されており、そのコードが 6c8c にコピーされました。

変更の背景

Go言語の初期のツールチェインは、Plan 9オペレーティングシステムのツールチェインをベースにしていました。6c, 8c, 5c といった名称は、それぞれAMD64 (x86-64), x86 (IA-32), ARMアーキテクチャ向けのCコンパイラを指します。これらのコンパイラは、GoプログラムがCコードと連携したり、Goランタイム自体がCで書かれた部分をコンパイルする際に使用されました。

このコミットの背景には、コンパイラが生成するコードの効率性、特にメモリ使用量の最適化があります。スタックフレームのサイズは、関数の呼び出しオーバーヘッドやメモリフットプリントに直接影響します。特に、Goランタイムの内部で使われるような低レベルのアセンブリ関数では、パフォーマンスとリソース効率が極めて重要になります。

#pragma textflag 7 は、Goのコンパイラとリンカに対する指示であり、特定の関数が「システムコール」のような特殊な性質を持つことを示唆します。このような関数は、スタックフレームのサイズに厳しい制約(128バイト)が課されることがあり、これは主にセキュリティやパフォーマンスの観点から、スタックオーバーフローのリスクを減らし、キャッシュ効率を向上させるためです。この制約を超過すると、コンパイルエラーや実行時エラーが発生する可能性があります。

したがって、このコミットは、コンパイラが生成するコードの品質を向上させ、特定の重要なGoランタイム関数が正しくコンパイルされ、効率的に実行されるようにするための重要なバグ修正でした。

前提知識の解説

スタックフレーム (Stack Frame)

関数が呼び出されるたびに、その関数に必要な情報(ローカル変数、引数、戻りアドレス、レジスタの退避領域など)を格納するために、プログラムのスタックメモリ上に確保される領域をスタックフレームと呼びます。スタックフレームは、関数が実行されている間だけ存在し、関数が終了すると解放されます。スタックフレームのサイズは、関数の効率性やメモリ使用量に直接影響します。

ローカル変数 (Automatic Variables)

C言語において、関数内で宣言された変数のうち、staticextern キーワードが付いていないものは「自動変数 (automatic variables)」と呼ばれます。これらは通常、関数のスタックフレーム内に割り当てられます。

アラインメント (Alignment)

コンピュータのメモリは、バイト単位でアドレス指定されますが、CPUが効率的にデータにアクセスするためには、特定のデータ型が特定のメモリアドレスの倍数に配置されている必要があります。これをアラインメントと呼びます。例えば、4バイトの整数は4の倍数のアドレスに配置されるべき、といったルールがあります。アラインメント要件を満たすために、コンパイラはメモリ領域の間にパディング(埋め草)を追加することがあります。

ptrsize (Pointer Size)

ポインタのサイズは、システムアーキテクチャによって異なります。32ビットシステムでは4バイト、64ビットシステムでは8バイトが一般的です。このコミットでは、SZ_VLONGSZ_LONG といったマクロがこれに相当する可能性が高いです。

xround 関数

コミットのコード変更箇所に見られる xround 関数は、おそらく指定されたアラインメント境界に値を切り上げるためのユーティリティ関数です。例えば、xround(v, SZ_LONG) は、vSZ_LONG の倍数に切り上げることを意味します。

#pragma textflag 7

Go言語のコンパイラにおける特殊なプラグマ(コンパイラへの指示)の一つです。textflag は、Goの関数がどのようにコンパイルされ、リンカによって扱われるかを制御します。textflag 7 は、特にGoランタイムの内部で使われるアセンブリ関数に対して適用されることが多く、これらの関数が非常に厳密なスタックフレームサイズの制約(通常128バイト)を持つことを示します。これは、システムコールのような低レベルの操作において、スタックのオーバーフローを防ぎ、パフォーマンスを最適化するために重要です。

5c, 6c, 8c

これらは、Go言語の初期のツールチェインに含まれていたPlan 9ベースのCコンパイラです。

  • 5c: ARMアーキテクチャ向けCコンパイラ
  • 6c: AMD64 (x86-64) アーキテクチャ向けCコンパイラ
  • 8c: x86 (IA-32) アーキテクチャ向けCコンパイラ

技術的詳細

このバグは、maxround という関数内で発生していました。この関数は、スタックフレームの最大サイズを計算する際に、ローカル変数や関数呼び出しに必要なスタック領域を適切にアラインメントし、切り上げる役割を担っています。

元のコードでは、maxround 関数内で v += SZ_VLONG-1; (または v += SZ_LONG-1; ) という操作が行われていました。ここで v は現在のスタック領域のサイズ、SZ_VLONG (または SZ_LONG) はワードサイズ(ポインタサイズ)を表します。

この v += SZ_VLONG-1; の意図は、vSZ_VLONG の倍数に切り上げるための準備として、vSZ_VLONG-1 を加算し、その後に切り捨てを行うことで、結果的に切り上げを実現するというものです。例えば、SZ_VLONG が8(8バイト)の場合、v が10であれば 10 + 7 = 17 となり、これを8で切り捨てると 16 となります。これは正しい切り上げです。

しかし、元のコードでは、この v += SZ_VLONG-1; の直後に xround を呼び出すのではなく、v を更新した後に再度 xround(v, SZ_VLONG) を呼び出していました。

元のコードのロジック:

  1. vSZ_VLONG-1 を加算する。
  2. vmax を比較し、vmax より大きければ maxxround(v, SZ_VLONG) で更新する。

問題点: v += SZ_VLONG-1; の時点で既に v が不必要に大きくなっており、その後の xround がさらに切り上げを行うため、結果的に SZ_VLONG 分の余分なスペースが確保されていました。これは、v が既に SZ_VLONG の倍数である場合でも発生し、常に1ワード分の無駄な領域が生じていました。

コミットメッセージの例: 「6c関数で1つのローカル変数と1つの2ワードの関数呼び出しを使用した場合、(8+8)+(16+8) = 40バイトではなく、8+16 = 24バイトを使用するべきだった。」

  • 8 はおそらくポインタサイズ(SZ_VLONG または SZ_LONG)。
  • 8+8 はローカル変数と何か(おそらくベースポインタや戻りアドレス)の合計。
  • 16+8 は2ワードの関数呼び出しに必要なスタック領域と何か。 この計算は、本来必要なサイズに加えて、余分な SZ_VLONG が加算されていたことを示唆しています。

修正は、v += SZ_VLONG-1; の代わりに v = xround(v, SZ_LONG); を直接使用することで、v を最初に正しいアラインメントに切り上げ、その後の処理で無駄な加算が行われないようにしました。これにより、スタックフレームのサイズが適切に計算され、無駄なパディングが削減されます。

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

src/cmd/6c/swt.c および src/cmd/8c/swt.cmaxround 関数。

--- a/src/cmd/6c/swt.c
+++ b/src/cmd/6c/swt.c
@@ -626,8 +626,8 @@ align(int32 i, Type *t, int op, int32 *maxalign)
 int32
 maxround(int32 max, int32 v)
 {
-	v += SZ_VLONG-1;
-	if(v > max)
-		max = xround(v, SZ_VLONG);
-	return max;
+	v = xround(v, SZ_LONG);
+	if(v > max)
+		return v;
+	return max;
 }

src/cmd/8c/swt.c も同様の変更です。

コアとなるコードの解説

変更された maxround 関数は、スタックフレームの最大サイズを計算する際に呼び出されます。

変更前:

int32
maxround(int32 max, int32 v)
{
	v += SZ_VLONG-1; // (1) ここでvを不必要に大きくする
	if(v > max)
		max = xround(v, SZ_VLONG); // (2) さらに切り上げを行う
	return max;
}
  1. v += SZ_VLONG-1;: vSZ_VLONG-1 を加算します。これは、vSZ_VLONG の倍数に切り上げるための一般的なテクニックですが、この文脈ではその後の xround と組み合わさることで冗長になります。
  2. max = xround(v, SZ_VLONG);: vSZ_VLONG の倍数に切り上げます。v は既にステップ(1)で大きくなっているため、結果として SZ_VLONG 分余分な領域が確保されてしまいます。

変更後:

int32
maxround(int32 max, int32 v)
{
	v = xround(v, SZ_LONG); // (1) vをSZ_LONGの倍数に直接切り上げる
	if(v > max)
		return v; // (2) vがmaxより大きければ、vをそのまま返す
	return max;
}
  1. v = xround(v, SZ_LONG);: vSZ_LONG (ポインタサイズ、またはワードサイズ) の倍数に直接切り上げます。これにより、v は常に適切なアラインメント境界に配置され、無駄な加算がなくなります。
  2. if(v > max) return v;: v が現在の max (これまでのスタックフレームの最大サイズ) よりも大きければ、新しい v が最大サイズとなるため、それを返します。そうでなければ、既存の max を返します。この変更により、max = xround(v, SZ_VLONG); のような冗長な代入がなくなり、ロジックがより直接的になりました。

この修正により、スタックフレームのサイズ計算が正確になり、特に #pragma textflag 7 のような厳しい制約を持つ関数において、不必要なスタック領域の消費を防ぐことができます。

関連リンク

  • Go言語のツールチェインに関する公式ドキュメント (当時のもの): Go言語の公式ドキュメントやブログ記事で、Goのコンパイラやリンカの内部構造について言及されているものがあれば参考になります。
  • Plan 9 from Bell Labs: Go言語のツールチェインのルーツであるPlan 9オペレーティングシステムに関する情報。

参考にした情報源リンク

  • Go言語のソースコードリポジトリ: https://github.com/golang/go
  • GoのIssueトラッカーやCL (Change List) の議論: このコミットに関連するCL (7303099) のページで、より詳細な議論や背景情報が見つかる可能性があります。
  • Goのコンパイラに関する技術記事やブログ: Goのコンパイラがどのようにスタックフレームを管理しているか、textflag の意味などについて解説している記事。
  • C言語のスタックフレームとアラインメントに関する一般的な情報源。
  • Plan 9 Cコンパイラに関する情報。
    • 5c, 6c, 8c の具体的な役割や歴史的背景について、Plan 9のドキュメントや関連する技術記事。