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

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

このコミットは、Go言語のリンカ(cmd/ld)において、関数エントリポイントのメモリ配置をアーキテクチャ固有の境界にアラインメント(整列)させる変更を導入しています。これにより、プログラムの実行性能の安定性と、一部のベンチマークにおける性能向上が期待されます。特に、x86アーキテクチャでは16バイト、ARMアーキテクチャでは4バイトのアラインメントが適用されます。

コミット

commit 8820ab5da9da5528e256d3a519723fdf44ddc75f
Author: Russ Cox <rsc@golang.org>
Date:   Wed May 30 16:26:38 2012 -0400

    cmd/ld: align function entry on arch-specific boundary
    
    16 seems pretty standard on x86 for function entry.
    I don't know if ARM would benefit, so I used just 4
    (single instruction alignment).
    
    This has a minor absolute effect on the current timings.
    The main hope is that it will make them more consistent from
    run to run.
    
    benchmark                 old ns/op    new ns/op    delta
    BenchmarkBinaryTree17    4222117400   4140739800   -1.93%
    BenchmarkFannkuch11      3462631800   3259914400   -5.85%
    BenchmarkGobDecode         20887622     20620222   -1.28%
    BenchmarkGobEncode          9548772      9384886   -1.72%
    BenchmarkGzip                151687       150333   -0.89%
    BenchmarkGunzip                8742         8741   -0.01%
    BenchmarkJSONEncode        62730560     65210990   +3.95%
    BenchmarkJSONDecode       252569180    249394860   -1.26%
    BenchmarkMandelbrot200      5267599      5273394   +0.11%
    BenchmarkRevcomp25M       980813500    996013800   +1.55%
    BenchmarkTemplate         361259100    360620840   -0.18%
    
    R=ken2
    CC=golang-dev
    https://golang.org/cl/6244066
---
 src/cmd/5l/l.h    | 3 ++-
 src/cmd/6l/l.h    | 3 ++-
 src/cmd/8l/l.h    | 3 ++-
 src/cmd/ld/data.c | 2 ++
 4 files changed, 8 insertions(+), 3 deletions(-)

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

https://github.com/golang/go/commit/8820ab5da9da5528e256d3a519723fdf44ddc75f

元コミット内容

cmd/ld: align function entry on arch-specific boundary

16 seems pretty standard on x86 for function entry.
I don't know if ARM would benefit, so I used just 4
(single instruction alignment).

This has a minor absolute effect on the current timings.
The main hope is that it will make them more consistent from
run to run.

benchmark                 old ns/op    new ns/op    delta
BenchmarkBinaryTree17    4222117400   4140739800   -1.93%
BenchmarkFannkuch11      3462631800   3259914400   -5.85%
BenchmarkGobDecode         20887622     20620222   -1.28%
BenchmarkGobEncode          9548772      9384886   -1.72%
BenchmarkGzip                151687       150333   -0.89%
BenchmarkGunzip                8742         8741   -0.01%
BenchmarkJSONEncode        62730560     65210990   +3.95%
BenchmarkJSONDecode       252569180    249394860   -1.26%
BenchmarkMandelbrot200      5267599      5273394   +0.11%
BenchmarkRevcomp25M       980813500    996013800   +1.55%
BenchmarkTemplate         361259100    360620840   -0.18%

R=ken2
CC=golang-dev
https://golang.org/cl/6244066

変更の背景

このコミットの主な目的は、Goプログラムの実行性能を向上させ、特にベンチマーク結果の一貫性を高めることです。関数エントリポイントのメモリ配置を特定の境界にアラインメントすることで、CPUのキャッシュ効率を最大化し、命令フェッチのオーバーヘッドを削減することが期待されます。

CPUはメモリから命令やデータを「キャッシュライン」と呼ばれる固定サイズのブロック単位で読み込みます。関数エントリポイントがキャッシュラインの境界にアラインされていない場合、関数の一部が複数のキャッシュラインにまたがって配置される可能性があります。これにより、CPUが関数を実行するために複数のキャッシュラインをフェッチする必要が生じ、性能が低下する可能性があります。

この変更は、特にx86アーキテクチャにおいて、関数エントリポイントを16バイト境界にアラインすることが標準的であるという知見に基づいています。ARMアーキテクチャについては、その時点での明確な性能上の利点が不明であったため、最小限の4バイト(単一命令のアラインメント)が選択されました。

コミットメッセージに含まれるベンチマーク結果は、この変更が一部のワークロードで実際に性能向上をもたらしていることを示しています。特にBenchmarkFannkuch11では5.85%の改善が見られます。また、性能の一貫性向上も重要な目標であり、これはベンチマークの信頼性を高める上で役立ちます。

前提知識の解説

1. 関数アラインメント (Function Alignment)

関数アラインメントとは、コンパイラやリンカが、実行可能ファイル内の関数の開始アドレスを、特定のバイト数の倍数(例えば16バイト、64バイトなど)に整列させる処理のことです。これは、CPUがメモリから命令を効率的にフェッチし、実行するために重要です。

2. キャッシュライン (Cache Line)

現代のCPUは、メインメモリよりも高速なキャッシュメモリ(L1, L2, L3キャッシュ)を搭載しています。CPUがメモリからデータを読み込む際、キャッシュは「キャッシュライン」と呼ばれる固定サイズのブロック単位でデータを転送します。一般的なキャッシュラインのサイズは64バイトです。

  • キャッシュヒット: 必要なデータがキャッシュ内に存在する場合、高速にアクセスできます。
  • キャッシュミス: 必要なデータがキャッシュ内に存在しない場合、メインメモリからキャッシュライン全体を読み込む必要があり、性能が低下します。

関数エントリポイントがキャッシュラインの境界にアラインされていると、関数がキャッシュラインの先頭から始まるため、CPUは一度のメモリアクセスで関数の大部分(または全体)をキャッシュに読み込むことができ、キャッシュミスを減らし、命令フェッチの効率を高めます。

3. リンカ (Linker)

リンカは、コンパイラによって生成されたオブジェクトファイル(コンパイルされたコードやデータを含む)を結合し、実行可能なプログラムを生成するツールです。リンカの主な役割は以下の通りです。

  • シンボル解決: 異なるオブジェクトファイル間で参照される関数や変数のアドレスを解決します。
  • メモリ配置: プログラムの各セクション(コード、データなど)をメモリ上のどこに配置するかを決定します。この際に、アラインメントの要件も考慮されます。
  • 実行可能ファイルの生成: 最終的な実行可能ファイルを生成します。

Go言語では、cmd/ld(またはcmd/link)がGo独自のリンカとして機能します。

4. Goツールチェインにおける 5l, 6l, 8l

Go言語の初期のツールチェインでは、異なるアーキテクチャ向けのリンカが個別のコマンドとして存在していました。これらはPlan 9オペレーティングシステムのツールチェインに由来します。

  • 5l: ARMアーキテクチャ向けのリンカ
  • 6l: AMD64 (x86-64) アーキテクチャ向けのリンカ
  • 8l: 386 (x86-32) アーキテクチャ向けのリンカ

現代のGo開発では、これらの個別のリンカコマンドを直接呼び出すことはほとんどありません。go buildコマンドが内部的に適切なリンカを呼び出し、ビルドプロセス全体を管理します。しかし、このコミットが作成された時点では、これらのリンカがまだ直接的に参照される構造になっていました。

技術的詳細

このコミットは、Go言語のリンカが、生成されるバイナリ内の関数エントリポイントのメモリ配置を最適化するものです。

x86アーキテクチャにおける関数アラインメント

x86プロセッサでは、関数エントリポイントを16バイト境界にアラインすることが一般的です。これは以下の理由によります。

  • キャッシュライン最適化: x86プロセッサのキャッシュラインは通常64バイトです。関数が16バイト境界にアラインされることで、関数の開始部分がキャッシュラインの先頭に配置されやすくなり、一度のキャッシュフェッチでより多くの命令が読み込まれる可能性が高まります。これにより、キャッシュミスが減少し、命令フェッチの効率が向上します。
  • 命令プリフェッチとデコード: x86-64アーキテクチャは、命令を16バイト単位でプリフェッチおよびデコードするように設計されています。関数エントリポイントがアラインされていると、プロセッサは整列された命令ブロックを効率的に処理できます。
  • SIMD命令の効率: SSEやAVXのようなSIMD(Single Instruction, Multiple Data)命令は、データだけでなく、それらを操作するコードも特定の境界にアラインされている場合に最高の性能を発揮します。

アラインメントを実現するためには、リンカが関数の前にパディング(詰め物)バイトを挿入することがあります。これにより、実行可能ファイルのサイズがわずかに増加する可能性がありますが、通常は性能向上によるメリットが上回ります。

ARMアーキテクチャにおける関数アラインメント

ARMアーキテクチャでは、命令は通常4バイト(32ビット)単位で配置されます。このコミットでは、ARMアーキテクチャ(5l)に対して4バイトのアラインメントが設定されています。これは「単一命令アラインメント」を意味し、各命令がその自然な境界に配置されることを保証します。

x86ほど厳密なキャッシュラインアラインメントの恩恵が明確でない場合でも、命令の自然な境界にアラインすることは、命令フェッチの効率とパイプラインの詰まりを避ける上で基本的な最適化となります。

リンカによるアラインメントの実現

リンカは、プログラムのセクションや個々のシンボル(関数など)をメモリに配置する際に、アラインメントの制約を考慮します。このコミットでは、リンカのデータ構造内でFuncAlignという定数を導入し、この値に基づいて関数のアドレスを調整しています。

具体的には、リンカがテキストセクション(実行可能コードを含むセクション)を構築する際に、各関数の開始アドレスがFuncAlignの倍数になるように調整されます。もし現在の仮想アドレス(va)がFuncAlignの倍数でない場合、リンカはrnd関数(おそらく「round up to nearest multiple」の略)を使用して、次のアラインされたアドレスにvaを移動させます。これにより、関数の前に必要なパディングが挿入されます。

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

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

  1. src/cmd/5l/l.h (ARMアーキテクチャ向けリンカのヘッダファイル)
  2. src/cmd/6l/l.h (AMD64アーキテクチャ向けリンカのヘッダファイル)
  3. src/cmd/8l/l.h (386アーキテクチャ向けリンカのヘッダファイル)
  4. src/cmd/ld/data.c (リンカのデータ処理に関するC言語ソースファイル)

src/cmd/5l/l.h の変更

--- a/src/cmd/5l/l.h
+++ b/src/cmd/5l/l.h
@@ -36,7 +36,8 @@
 enum
 {
 	thechar = '5',
-	PtrSize = 4
+	PtrSize = 4,
+	FuncAlign = 4  // single-instruction alignment
 };

FuncAlign = 4 が追加されました。これはARMアーキテクチャにおいて、関数エントリポイントを4バイト境界にアラインすることを指定します。コメントで「single-instruction alignment」とあるように、命令単位でのアラインメントを意図しています。

src/cmd/6l/l.h の変更

--- a/src/cmd/6l/l.h
+++ b/src/cmd/6l/l.h
@@ -40,7 +40,8 @@
 enum
 {
 	thechar = '6',
-	PtrSize = 8
+	PtrSize = 8,
+	FuncAlign = 16
 };

FuncAlign = 16 が追加されました。これはAMD64 (x86-64) アーキテクチャにおいて、関数エントリポイントを16バイト境界にアラインすることを指定します。

src/cmd/8l/l.h の変更

--- a/src/cmd/8l/l.h
+++ b/src/cmd/8l/l.h
@@ -40,7 +40,8 @@
 enum
 {
 	thechar = '8',
-	PtrSize = 4
+	PtrSize = 4,
+	FuncAlign = 16
 };

FuncAlign = 16 が追加されました。これは386 (x86-32) アーキテクチャにおいて、関数エントリポイントを16バイト境界にアラインすることを指定します。

src/cmd/ld/data.c の変更

--- a/src/cmd/ld/data.c
+++ b/src/cmd/ld/data.c
@@ -1012,6 +1012,8 @@ textaddress(void)
 			continue;
 		if(sym->align != 0)
 			va = rnd(va, sym->align);
+		else if(sym->text != P)
+			va = rnd(va, FuncAlign);
 		sym->value = 0;
 		for(sub = sym; sub != S; sub = sub->sub) {
 			sub->value += va;

textaddress 関数内で、シンボルのアラインメント処理が変更されました。 既存の if(sym->align != 0) ブロックは、シンボル自体に特定のアラインメント要件がある場合(例えば、データ構造など)に適用されます。 追加された else if(sym->text != P) ブロックは、シンボルがテキストセクション(つまり関数)であり、かつ明示的なアラインメントが指定されていない場合に適用されます。この場合、新しく定義された FuncAlign の値に基づいて、現在の仮想アドレス va がアラインされます。sym->text != P は、シンボルがコード(関数)であることを示しています。

コアとなるコードの解説

この変更の核心は、各アーキテクチャのヘッダファイルで定義された FuncAlign 定数と、リンカの textaddress 関数におけるその利用です。

FuncAlign は、Goのリンカが関数をメモリに配置する際に使用するアラインメント境界を定義します。

  • src/cmd/5l/l.h (ARM): FuncAlign = 4 ARMプロセッサの命令は通常4バイト長であるため、4バイトアラインメントは各命令がメモリ上で自然な境界に配置されることを保証します。これは基本的な命令フェッチ効率を確保するために重要です。

  • src/cmd/6l/l.h (AMD64) および src/cmd/8l/l.h (386): FuncAlign = 16 x86系のプロセッサでは、16バイトアラインメントが選択されています。これは、CPUのキャッシュライン(通常64バイト)や命令プリフェッチユニットの動作に最適化された値です。関数エントリポイントが16バイト境界にアラインされることで、CPUが一度にキャッシュに読み込む命令のブロックが効率的に配置され、キャッシュミスや命令フェッチの遅延が減少します。

src/cmd/ld/data.ctextaddress 関数は、リンカがテキストセクション内のシンボル(関数など)にアドレスを割り当てる主要なロジックを含んでいます。

		if(sym->align != 0)
			va = rnd(va, sym->align);
		else if(sym->text != P)
			va = rnd(va, FuncAlign);

このコードスニペットは、シンボル sym の仮想アドレス va を計算する部分です。

  1. if(sym->align != 0): もしシンボル自体に明示的なアラインメント要件(例えば、特定のデータ構造が持つアラインメント属性など)が設定されている場合、その要件に従って va をアラインします。rnd 関数は、va を指定されたアラインメントの次の倍数に切り上げる役割を果たします。
  2. else if(sym->text != P): 上記の条件が満たされず、かつシンボルがテキストセクション(つまり関数)である場合(sym->text != P が真の場合)、FuncAlign の値を使用して va をアラインします。これにより、すべての関数エントリポイントが、そのアーキテクチャで定義された FuncAlign の境界に整列されることが保証されます。

この変更により、Goのリンカは、生成されるバイナリの関数配置をよりハードウェアに最適化された形で行うようになり、結果として実行性能の向上と安定性をもたらします。

関連リンク

参考にした情報源リンク