[インデックス 13193] ファイルの概要
このコミットは、Go言語のランタイムにおけるパフォーマンス最適化を目的としています。具体的には、アライメント計算に使用されていた runtime·rnd
関数を、より高速な ROUND
マクロに置き換えることで、プロファイル上で頻繁に現れる関数呼び出しのオーバーヘッドを削減しています。
コミット
commit 6dbaa206fbe7345fac181ec3d91a4157d5532fbd
Author: Russ Cox <rsc@golang.org>
Date: Tue May 29 14:02:29 2012 -0400
runtime: replace runtime·rnd function with ROUND macro
It's sad to introduce a new macro, but rnd shows up consistently
in profiles, and the function call overwhelms the two arithmetic
instructions it performs.
R=r
CC=golang-dev
https://golang.org/cl/6260051
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/6dbaa206fbe7345fac181ec3d91a4157d5532fbd
元コミット内容
runtime: replace runtime·rnd function with ROUND macro
It's sad to introduce a new macro, but rnd shows up consistently
in profiles, and the function call overwhelms the two arithmetic
instructions it performs.
R=r
CC=golang-dev
https://golang.org/cl/6260051
変更の背景
この変更の主な背景は、Goランタイムのパフォーマンス最適化です。コミットメッセージに明記されているように、runtime·rnd
関数がプロファイル(パフォーマンス測定)において一貫して高い頻度で出現し、その関数呼び出しのオーバーヘッドが、関数内で実際に行われるわずか2つの算術演算のコストを上回っていました。
Go言語のようなシステムプログラミング言語では、ランタイムの効率性が全体のパフォーマンスに大きく影響します。特に、メモリのアライメント計算のような低レベルで頻繁に実行される操作は、わずかなオーバーヘッドでも累積すると無視できないボトルネックとなる可能性があります。関数呼び出しは、引数のプッシュ、スタックフレームのセットアップ、レジスタの保存・復元、ジャンプ命令など、一定のコストを伴います。runtime·rnd
のように非常に単純な処理を行う関数では、この呼び出しコストが実処理のコストを上回ってしまう「関数呼び出しのオーバーヘッド」が問題となります。
この問題を解決するため、関数呼び出しのオーバーヘッドを完全に排除できるマクロへの置き換えが選択されました。これにより、rnd
の処理がインライン展開され、コンパイル時に直接コードに埋め込まれるため、実行時のパフォーマンスが向上します。
前提知識の解説
Goランタイム (Go Runtime)
Goランタイムは、Goプログラムの実行を管理するシステムです。これには、ガベージコレクション、スケジューラ(ゴルーチンの管理)、メモリ割り当て、チャネル通信、インターフェースのディスパッチなど、Go言語の並行性モデルとメモリ管理を支える多くの低レベル機能が含まれます。ランタイムはGoプログラムに静的にリンクされ、Goプログラムの一部として実行されます。src/pkg/runtime
ディレクトリには、これらのランタイム機能の実装が含まれています。
関数呼び出しのオーバーヘッド
プログラミングにおいて、関数(またはサブルーチン)を呼び出す際には、その関数を実行するために必要な追加の処理が発生します。これを「関数呼び出しのオーバーヘッド」と呼びます。一般的なオーバーヘッドには以下が含まれます。
- スタックフレームのセットアップ: 呼び出し元の関数の状態(レジスタの値など)を保存し、呼び出される関数のローカル変数や引数のための新しいスタックフレームを割り当てる。
- 引数の渡し: 呼び出される関数に引数を渡す(レジスタまたはスタック経由)。
- ジャンプ命令: 呼び出される関数の開始アドレスへプログラムカウンタを移動させる。
- レジスタの保存/復元: 呼び出し規約によっては、特定のレジスタを呼び出し元または呼び出し先で保存・復元する必要がある。
- スタックフレームの破棄: 関数が終了する際に、スタックフレームを解放し、呼び出し元の状態を復元する。
これらの処理は、関数が実行する実際のロジックが非常に単純な場合、そのロジック自体の実行時間よりも長くなることがあります。
マクロ (Macro)
C言語やGo言語のランタイムの一部で使われるC言語のプリプロセッサマクロは、コンパイルのプリプロセス段階でテキスト置換を行う機能です。関数とは異なり、マクロは関数呼び出しのオーバーヘッドを発生させません。マクロが使用される箇所では、そのマクロの定義内容が直接コードに展開(インライン展開)されます。
マクロの利点:
- パフォーマンス向上: 関数呼び出しのオーバーヘッドがないため、特に頻繁に呼び出される小さな処理で高速化が期待できます。
- 型に依存しない: 型チェックはプリプロセス後に行われるため、ジェネリックな処理を記述しやすい場合があります(ただし、型安全性の問題を引き起こす可能性もある)。
マクロの欠点:
- デバッグの困難さ: プリプロセスで展開されるため、デバッガでステップ実行する際に元のマクロ定義が見えにくいことがあります。
- コードサイズの増大: 繰り返し展開されると、実行可能ファイルのサイズが増大する可能性があります。
- 予期せぬ副作用: 引数が複数回評価されるなど、マクロの展開方法によっては予期せぬ動作を引き起こすことがあります。
このコミットでは、パフォーマンス上の利点が欠点を上回ると判断され、マクロの導入が正当化されています。
メモリのアライメント (Memory Alignment)
メモリのアライメントとは、データがメモリ上で特定のバイト境界に配置されることを指します。多くのCPUアーキテクチャでは、特定のデータ型(例: 4バイト整数、8バイトポインタ)がそのサイズと同じバイト数の倍数のアドレスに配置されている場合に、最も効率的にアクセスできます。アライメントされていないデータへのアクセスは、パフォーマンスの低下や、場合によってはハードウェアエラーを引き起こす可能性があります。
runtime·rnd
関数(そして新しい ROUND
マクロ)は、このメモリのアライメントを考慮して、与えられたサイズを指定された境界(m
)の倍数に切り上げる(ラウンドアップする)ために使用されていました。例えば、m
が8バイトのアライメント境界を表す場合、rnd(n, 8)
は n
を8の倍数に切り上げた値を返します。
技術的詳細
このコミットの核心は、runtime·rnd
関数を ROUND
マクロに置き換えることで、アライメント計算のパフォーマンスを向上させる点にあります。
runtime·rnd
関数の実装 (削除前)
削除された runtime·rnd
関数の実装は src/pkg/runtime/runtime.c
にありました。
uint32
runtime·rnd(uint32 n, uint32 m)
{
uint32 r;
if(m > maxround)
m = maxround;
r = n % m;
if(r)
n += m-r;
return n;
}
この関数は、n
を m
の倍数に切り上げる処理を行っています。
r = n % m;
でn
をm
で割った余りを計算します。- もし余り
r
が0でなければ(つまりn
がm
の倍数でなければ)、n
にm-r
を加算します。これによりn
は次のm
の倍数に切り上げられます。 maxround
との比較は、m
が非常に大きい値の場合の安全策です。
この関数は、非常に単純な算術演算(剰余、減算、加算)しか行っていませんが、関数呼び出しのオーバーヘッドがその実行コストを上回っていたため、プロファイル上でボトルネックとして検出されました。
ROUND
マクロの実装 (新規追加)
新しい ROUND
マクロは src/pkg/runtime/runtime.h
に追加されました。
#define ROUND(x, n) (((x)+(n)-1)&~((n)-1)) /* all-caps to mark as macro: it evaluates n twice */
このマクロは、x
を n
の倍数に切り上げるためのビット演算トリックを使用しています。ここで n
は2のべき乗であると仮定されています(アライメント境界は通常2のべき乗です)。
n-1
:n
が2のべき乗の場合、n-1
はn
の下位ビットがすべて1になるような値になります。例えば、n=8 (0b1000)
の場合、n-1=7 (0b0111)
です。~(n-1)
:n-1
のビットを反転させると、n
のビット位置より下位のビットがすべて0になり、n
のビット位置以上のビットがすべて1になります。例えば、n=8 (0b1000)
の場合、~(n-1)
は~0b0111
となり、これは...11111000
となります。これは、n
の倍数に切り捨てるためのマスクとして機能します。(x)+(n)-1
:x
にn-1
を加算することで、x
がn
の倍数でない場合でも、次のn
の倍数に「到達」するようにします。例えば、x=5, n=8
の場合、5 + 8 - 1 = 12 (0b1100)
となります。((x)+(n)-1)&~((n)-1)
: 最後に、この加算結果を~(n-1)
でビットAND演算します。これにより、n
の倍数に切り捨てられます。結果として、元のx
がn
の倍数に切り上げられた値が得られます。- 例:
x=5, n=8
(x)+(n)-1
=5 + 8 - 1 = 12 (0b1100)
~(n-1)
=~7 = ~0b0111 = ...11111000
0b1100 & ...11111000 = 0b1000 = 8
(正しく切り上げられた)
- 例:
x=8, n=8
(x)+(n)-1
=8 + 8 - 1 = 15 (0b1111)
~(n-1)
=~7 = ~0b0111 = ...11111000
0b1111 & ...11111000 = 0b1000 = 8
(正しく切り上げられた)
- 例:
このビット演算によるアライメント計算は、関数呼び出しを伴わないため、非常に高速です。ただし、コメントにもあるように n
が2回評価される可能性があるため、n
に副作用のある式を渡すべきではありません。しかし、このユースケースでは n
は定数または単純な変数であり、問題ありません。
コアとなるコードの変更箇所
このコミットでは、以下のファイルが変更されています。
src/cmd/gc/reflect.c
: Goコンパイラの反射関連コード。アライメント計算にruntime·rnd
を使用していた箇所がROUND
に変更されています。src/pkg/runtime/chan.c
: Goランタイムのチャネル関連コード。チャネルの要素サイズや選択処理におけるアライメント計算が変更されています。src/pkg/runtime/hashmap.c
: Goランタイムのハッシュマップ(マップ)関連コード。マップのキーや値のサイズ計算におけるアライメント計算が変更されています。src/pkg/runtime/iface.c
: Goランタイムのインターフェース関連コード。インターフェースのデータ構造におけるアライメント計算が変更されています。src/pkg/runtime/print.c
: Goランタイムのデバッグ出力(printf
など)関連コード。可変引数のアライメント計算が変更されています。src/pkg/runtime/runtime.c
:runtime·rnd
関数の定義が削除されました。src/pkg/runtime/runtime.h
:ROUND
マクロの定義が追加されました。
変更の概要は、runtime·rnd(A, B)
の形式の関数呼び出しが ROUND(A, B)
の形式のマクロ呼び出しに置き換えられ、同時に runtime·rnd
関数の実装が削除されたことです。
コアとなるコードの解説
src/pkg/runtime/runtime.c
から runtime·rnd
関数の削除
--- a/src/pkg/runtime/runtime.c
+++ b/src/pkg/runtime/runtime.c
@@ -156,19 +156,6 @@ runtime·mchr(byte *p, byte c, byte *ep)
return nil;
}
-uint32
-runtime·rnd(uint32 n, uint32 m)
-{
- uint32 r;
-
- if(m > maxround)
- m = maxround;
- r = n % m;
- if(r)
- n += m-r;
- return n;
-}
-
static int32 argc;
static uint8** argv;
この差分は、runtime·rnd
関数の実装が runtime.c
から完全に削除されたことを示しています。これは、この関数がもはや必要なくなり、その機能がマクロに置き換えられたためです。
src/pkg/runtime/runtime.h
への ROUND
マクロの追加
--- a/src/pkg/runtime/runtime.h
+++ b/src/pkg/runtime/runtime.h
@@ -390,6 +390,7 @@ struct ParFor
#define nelem(x) (sizeof(x)/sizeof((x)[0]))
#define nil ((void*)0)
#define offsetof(s,m) (uint32)(&(((s*)0)->m))
+#define ROUND(x, n) (((x)+(n)-1)&~((n)-1)) /* all-caps to mark as macro: it evaluates n twice */
/*
* known to compiler
@@ -533,7 +534,6 @@ void runtime·goenvs_unix(void);
void* runtime·getu(void);
void runtime·throw(int8*);
void runtime·panicstring(int8*);
-uint32 runtime·rnd(uint32, uint32);
void runtime·prints(int8*);
void runtime·printf(int8*, ...);
byte* runtime·mchr(byte*, byte, byte*);
この差分は、runtime.h
に ROUND
マクロが新しく定義されたことを示しています。また、runtime·rnd
関数のプロトタイプ宣言も削除されています。これにより、コンパイラは runtime·rnd
が存在しないことを認識し、代わりに ROUND
マクロが使用されるようになります。
各ファイルでの runtime·rnd
から ROUND
への置き換え例
例えば、src/pkg/runtime/chan.c
の以下の行が変更されています。
--- a/src/pkg/runtime/chan.c
+++ b/src/pkg/runtime/chan.c
@@ -446,7 +446,7 @@ runtime·selectnbsend(ChanType *t, Hchan *c, ...)\n \tbyte *ae, *ap;\n \n \tae = (byte*)(&c + 1);\n-\tap = ae + runtime·rnd(t->elem->size, Structrnd);\n+\tap = ae + ROUND(t->elem->size, Structrnd);\n \truntime·chansend(t, c, ae, ap);\n }\n \n```
ここでは、チャネルの要素サイズ `t->elem->size` を `Structrnd` (構造体のアライメント境界) に合わせて切り上げるために `runtime·rnd` が使われていましたが、これが `ROUND` マクロに置き換えられています。同様の変更が、`reflect.c`, `hashmap.c`, `iface.c`, `print.c` の各ファイルで行われています。
これらの変更により、Goランタイムの様々な箇所で行われていたアライメント計算が、関数呼び出しのオーバーヘッドなしに、より効率的なビット演算によって実行されるようになりました。これは、Goプログラム全体のパフォーマンス向上に寄与する重要な最適化です。
## 関連リンク
* Go言語の公式ドキュメント: [https://golang.org/](https://golang.org/)
* Goのソースコードリポジトリ: [https://github.com/golang/go](https://github.com/golang/go)
## 参考にした情報源リンク
* Go CL (Change List) 6260051: [https://golang.org/cl/6260051](https://golang.org/cl/6260051) (コミットメッセージに記載されている元の変更リスト)
* ビット演算によるアライメント: 一般的なプログラミングの最適化手法として知られています。