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

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

このコミットは、Go言語のリンカ(5l8l)におけるI/O処理の最適化に関するものです。具体的には、文字を読み込む関数呼び出しをマクロに置き換えることで、リンカの処理速度を約2%向上させています。

コミット

commit f47346c5fce2eafc57bd0b6da14d531a49237345
Author: Jan Ziak <0xe2.0x9a.0x9b@gmail.com>
Date:   Tue May 15 12:52:18 2012 -0400

    8l,5l: 2% faster
    
    R=golang-dev, for.go.yong
    CC=golang-dev
    https://golang.org/cl/6197080

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

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

元コミット内容

コミットメッセージは「8l,5l: 2% faster」と非常に簡潔です。これは、Go言語のリンカである8l(x86アーキテクチャ向け)と5l(ARMアーキテクチャ向け)のパフォーマンスが2%向上したことを示しています。

変更の背景

この変更の背景には、Go言語のリンカのパフォーマンス最適化があります。リンカは、コンパイルされたオブジェクトファイルを結合して実行可能ファイルを生成する重要なツールであり、その速度は開発者のビルド時間に直接影響します。特に、大規模なプロジェクトではリンカの処理時間が無視できないほど長くなることがあります。

このコミットでは、リンカがファイルからバイトを読み取る際の効率を改善することを目指しています。元のコードではBgetcという関数が使用されていましたが、これをBGETCというマクロに置き換えることで、関数呼び出しのオーバーヘッドを削減し、全体的な処理速度の向上を図っています。2%という改善は一見小さいように見えますが、リンカのような頻繁に実行されるツールにおいては、このような小さな最適化の積み重ねが全体の開発体験に大きな影響を与えることがあります。

前提知識の解説

Go言語のリンカ (5l, 8l)

Go言語のツールチェインには、各アーキテクチャに対応するリンカが存在します。

  • 5l: ARMアーキテクチャ向けのリンカ。
  • 8l: x86(32ビットおよび64ビット)アーキテクチャ向けのリンカ。 これらのリンカは、Goコンパイラによって生成されたオブジェクトファイル(.oファイル)を結合し、最終的な実行可能バイナリを生成する役割を担っています。

BiobufとバッファリングI/O

Biobufは、Goリンカ内で使用されるバッファリングI/O(入出力)のメカニズムに関連する構造体です。バッファリングI/Oは、ディスクやネットワークなどの低速なI/Oデバイスとのやり取りの効率を高めるための一般的な手法です。

  • バッファリングの原理: 小さな読み書き操作をメモリ上のバッファに一時的に蓄積し、バッファがいっぱいになったり、特定の条件が満たされたりしたときに、まとめて大きな単位で実際のシステムコール(OSへのI/O要求)を発行します。
  • パフォーマンス向上: システムコールは、ユーザー空間からカーネル空間へのコンテキストスイッチを伴うため、CPUサイクルを消費し、レイテンシを発生させます。バッファリングにより、システムコールの回数を減らすことで、これらのオーバーヘッドを削減し、I/Oスループットを向上させることができます。

BgetcBGETC

  • Bgetc: Biobufから1バイトを読み取るための関数です。関数呼び出しには、スタックフレームのセットアップ、引数の渡し、リターンアドレスの保存などのオーバーヘッドが伴います。
  • BGETC: このコミットで導入された変更から推測すると、BGETCBgetcの機能をインラインで展開するマクロである可能性が高いです。マクロはプリプロセッサによってコンパイル時に展開されるため、実行時の関数呼び出しオーバーヘッドが発生しません。これにより、頻繁に呼び出されるI/O操作において、わずかながらもパフォーマンスの向上が期待できます。

技術的詳細

このコミットの技術的な核心は、GoリンカのI/O処理におけるマイクロ最適化です。src/cmd/5l/obj.csrc/cmd/8l/obj.cの2つのファイルで、Bgetc(f)という関数呼び出しがBGETC(f)という形式に一括して置き換えられています。

これは、C言語における関数とマクロのパフォーマンス特性の違いを利用した最適化です。

  • 関数呼び出しのオーバーヘッド: Bgetcが通常の関数として実装されている場合、その呼び出しごとにCPUは以下の処理を行います。

    • 呼び出し元のレジスタの状態を保存
    • 引数をスタックまたはレジスタにプッシュ
    • 関数アドレスへのジャンプ
    • 関数内のローカル変数のためのスタックフレームをセットアップ
    • 関数の実行
    • 戻り値を設定
    • 呼び出し元のレジスタの状態を復元
    • 呼び出し元へのリターン これらの処理は、個々には非常に高速ですが、リンカのように大量のバイトを読み込む処理では、Bgetcが何百万回も呼び出される可能性があり、その合計オーバーヘッドは無視できないものとなります。
  • マクロによる最適化: BGETCがマクロとして定義されている場合、プリプロセッサはコンパイル時にBGETC(f)という記述を、Bgetc関数の実体(例えば、バッファから直接バイトを読み取るコード)に置き換えます。これにより、実行時には関数呼び出しのオーバーヘッドが完全に排除されます。コードがインライン展開されるため、CPUのパイプライン予測も改善され、キャッシュ効率も向上する可能性があります。

この変更は、リンカがオブジェクトファイルを解析し、シンボルやアドレス情報を読み取る際に、Biobufからバイト単位でデータを取得する処理の効率を直接的に改善します。特に、zaddr関数(アドレスの解析)やメインの読み込みループ(loopラベルの箇所)など、頻繁にバイト読み込みが行われる箇所でこの最適化が適用されています。

結果として、リンカの実行時間が短縮され、コミットメッセージにあるように「2% faster」というパフォーマンス向上が実現されました。これは、Go言語のツールチェインが継続的にパフォーマンス改善に取り組んでいる一例であり、低レベルなI/O操作の最適化がいかに重要であるかを示しています。

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

変更は主にsrc/cmd/5l/obj.csrc/cmd/8l/obj.cの2つのファイルにわたります。

src/cmd/5l/obj.c

--- a/src/cmd/5l/obj.c
+++ b/src/cmd/5l/obj.c
@@ -296,16 +296,16 @@ zaddr(Biobuf *f, Adr *a, Sym *h[])
 	Sym *s;
 	Auto *u;
 
-	a->type = Bgetc(f);
-	a->reg = Bgetc(f);
-	c = Bgetc(f);
+	a->type = BGETC(f);
+	a->reg = BGETC(f);
+	c = BGETC(f);
 	if(c < 0 || c > NSYM){
 		print("sym out of range: %d\n", c);
 		Bputc(f, ALAST+1);
 		return;
 	}
 	a->sym = h[c];
-	a->name = Bgetc(f);
+	a->name = BGETC(f);
 
 	if((schar)a->reg < 0 || a->reg > NREG) {
 		print("register out of range %d\n", a->reg);
@@ -338,7 +338,7 @@ zaddr(Biobuf *f, Adr *a, Sym *h[])
 		break;
 
 	case D_REGREG:
-		a->offset = Bgetc(f);
+		a->offset = BGETC(f);
 		break;
 
 	case D_CONST2:
@@ -422,7 +422,7 @@ newloop:
 loop:
 	if(f->state == Bracteof || Boffset(f) >= eof)
 		goto eof;
-	o = Bgetc(f);
+	o = BGETC(f);
 	if(o == Beof)
 		goto eof;
 
@@ -435,8 +435,8 @@ loop:
 		sig = 0;
 		if(o == ASIGNAME)
 			sig = Bget4(f);
-		v = Bgetc(f); /* type */
-		o = Bgetc(f); /* sym */
+		v = BGETC(f); /* type */
+		o = BGETC(f); /* sym */
 		r = 0;
 		if(v == D_STATIC)
 			r = version;
@@ -486,8 +486,8 @@ loop:
 
 	p = mal(sizeof(Prog));
 	p->as = o;
-	p->scond = Bgetc(f);
-	p->reg = Bgetc(f);
+	p->scond = BGETC(f);
+	p->reg = BGETC(f);
 	p->line = Bget4(f);
 
 	zaddr(f, &p->from, h);

src/cmd/8l/obj.c

--- a/src/cmd/8l/obj.c
+++ b/src/cmd/8l/obj.c
@@ -333,7 +333,7 @@ zsym(char *pn, Biobuf *f, Sym *h[])
 {	
 	int o;
 	
-	o = Bgetc(f);
+	o = BGETC(f);
 	if(o < 0 || o >= NSYM || h[o] == nil)
 		mangle(pn);
 	return h[o];
@@ -347,12 +347,12 @@ zaddr(char *pn, Biobuf *f, Adr *a, Sym *h[])
 	Sym *s;
 	Auto *u;
 
-	t = Bgetc(f);
+	t = BGETC(f);
 	a->index = D_NONE;
 	a->scale = 0;
 	if(t & T_INDEX) {
-		a->index = Bgetc(f);
-		a->scale = Bgetc(f);
+		a->index = BGETC(f);
+		a->scale = BGETC(f);
 	}
 	a->type = D_NONE;
 	a->offset = 0;
@@ -376,7 +376,7 @@ zaddr(char *pn, Biobuf *f, Adr *a, Sym *h[])
 		a->type = D_SCONST;
 	}
 	if(t & T_TYPE)
-		a->type = Bgetc(f);
+		a->type = BGETC(f);
 	adrgotype = S;
 	if(t & T_GOTYPE)
 		adrgotype = zsym(pn, f, h);
@@ -452,10 +452,10 @@ newloop:
 loop:
 	if(f->state == Bracteof || Boffset(f) >= eof)
 		goto eof;
-	o = Bgetc(f);
+	o = BGETC(f);
 	if(o == Beof)
 		goto eof;
-	o |= Bgetc(f) << 8;
+	o |= BGETC(f) << 8;
 	if(o <= AXXX || o >= ALAST) {
 		if(o < 0)
 			goto eof;
@@ -468,8 +468,8 @@ loop:
 		sig = 0;
 		if(o == ASIGNAME)
 			sig = Bget4(f);
-		v = Bgetc(f);	/* type */
-		o = Bgetc(f);	/* sym */
+		v = BGETC(f);	/* type */
+		o = BGETC(f);	/* sym */
 		r = 0;
 		if(v == D_STATIC)
 			r = version;

コアとなるコードの解説

上記の差分が示すように、変更は非常にシンプルです。Bgetc(f)という関数呼び出しが、すべてBGETC(f)という形式に置き換えられています。

この変更の背後にある仮定は、BGETCBgetcのインラインバージョン、おそらくはマクロとして定義されているということです。C言語では、マクロはプリプロセッサによってコンパイル前に展開されるため、実行時には関数呼び出しのオーバーヘッドが発生しません。

例えば、BGETCが以下のように定義されていると仮定できます(実際の定義はGoのソースコード内で確認する必要がありますが、一般的なパターンです):

#define BGETC(f) ((f)->rp < (f)->wp ? *(f)->rp++ : Bgetc(f))

このマクロは、バッファ(f->rpからf->wpまでの範囲)にまだ読み取り可能なデータがある場合は、直接バッファから1バイトを読み取り(*(f)->rp++)、ポインタを進めます。これにより、関数呼び出しなしで高速にバイトを取得できます。バッファが空の場合にのみ、実際のBgetc関数(またはそれに相当する低レベルの読み取り関数)が呼び出され、バッファを補充します。

このように、ほとんどのケースで関数呼び出しを回避し、直接バッファから読み取ることで、リンカが大量のバイトを処理する際のCPUサイクルを節約し、全体的なパフォーマンスを向上させています。これは、I/Oバウンドな処理において非常に効果的な最適化手法です。

関連リンク

  • Go言語の公式リポジトリ: https://github.com/golang/go
  • Go言語のリンカに関するドキュメント(一般的な情報源): Goのリンカは内部ツールであり、詳細な公式ドキュメントは少ないですが、Goのソースコード自体が最も正確な情報源となります。

参考にした情報源リンク