[インデックス 16912] ファイルの概要
このコミットは、GoランタイムにおけるARMアーキテクチャ向けの64ビット整数演算の最適化と、特定のコンパイラ生成コードの回避を目的としています。具体的には、runtime.c
内のtimediv
関数における64ビット除算のコードを調整し、_vasop
という内部ヘルパー関数の呼び出しを回避します。また、vlrt_arm.c
内の64ビット減算関数_subv
を最適化し、特定のコンパイラフラグを付与することで、その利用を「問題ない」状態にします。
コミット
commit c7d5c438a2bd6bad779cc535d11466bb93cc08e3
Author: Russ Cox <rsc@golang.org>
Date: Mon Jul 29 16:42:07 2013 -0400
runtime: adjust timediv to avoid _vasop; mark _subv okay
R=dvyukov
CC=golang-dev
https://golang.org/cl/12028046
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/c7d5c438a2bd6bad779cc535d11466bb93cc08e3
元コミット内容
runtime: adjust timediv to avoid _vasop; mark _subv okay
R=dvyukov
CC=golang-dev
https://golang.org/cl/12028046
変更の背景
このコミットの背景には、GoランタイムがARMアーキテクチャ上で64ビット整数演算を効率的に実行するための課題があります。当時のARMプロセッサ(特にARMv7のような32ビットアーキテクチャ)は、ネイティブで64ビット整数を直接扱う命令が限られていました。そのため、64ビットの加算、減算、除算といった操作は、複数の32ビットレジスタとキャリー/ボローフラグを組み合わせた複雑なシーケンス、あるいはコンパイラが生成するランタイムヘルパー関数を介して行われることが一般的でした。
_vasop
という用語はGoランタイムの内部的なものであり、具体的な定義は公開されていませんが、文脈から判断すると、これは「可変長オペレーション」または「ベクトル演算オペレーション」のような、特定の複雑な算術操作を処理するためのコンパイラが生成する(またはランタイムが提供する)ヘルパー関数であると推測されます。このようなヘルパー関数を呼び出すことは、関数呼び出しのオーバーヘッドや、インライン化されないことによるパフォーマンスの低下を招く可能性があります。
このコミットは、timediv
関数における64ビット除算のコードが、意図せず_vasop
ヘルパーを呼び出すようなコードをコンパイラが生成するのを防ぐことを目的としています。また、_subv
関数についても、その実装を最適化し、特定のコンパイラ最適化フラグを付与することで、より効率的に利用できるようにすることが狙いです。これは、Goランタイムのパフォーマンス、特に低レベルの算術演算が頻繁に行われる部分での効率を向上させるための、マイクロ最適化の一環と考えられます。
前提知識の解説
Goランタイム (Go Runtime)
Goランタイムは、Goプログラムの実行を管理する低レベルのシステムです。これには、ゴルーチン(軽量スレッド)のスケジューリング、ガベージコレクション、メモリ管理、システムコールインターフェースなどが含まれます。Goプログラムは、コンパイル時にGoランタイムとリンクされ、ランタイムの機能を利用して実行されます。パフォーマンスが重要なため、ランタイムのコードはC言語やアセンブリ言語で書かれている部分も多く、特定のアーキテクチャに特化した最適化が行われることがあります。
ARMアーキテクチャ
ARM(Advanced RISC Machine)は、モバイルデバイスや組み込みシステムで広く使用されているCPUアーキテクチャです。このコミットが作成された2013年頃は、ARMv7のような32ビットアーキテクチャが主流でした。32ビットアーキテクチャでは、レジスタの幅が32ビットであるため、64ビット整数(int64
やuint64
)を直接扱うことができません。
32ビットシステムにおける64ビット整数演算
32ビットシステムで64ビット整数を扱う場合、通常は64ビットの値を2つの32ビットワード(上位ワードと下位ワード)に分割して表現します。例えば、Vlong
構造体はlo
(下位32ビット)とhi
(上位32ビット)の2つのulong
(符号なし32ビット整数)で64ビット値を表現しています。
64ビットの加算や減算を行う際には、下位ワードの演算結果からキャリー(繰り上がり)やボロー(借り入れ)が発生した場合、それを上位ワードの演算に反映させる必要があります。除算のようなより複雑な操作では、さらに多くのステップや、コンパイラが提供する特殊なヘルパー関数が必要になることがあります。
コンパイラ最適化とヘルパー関数
C言語のような低レベル言語で書かれたコードは、コンパイラによって機械語に変換されます。この際、コンパイラはコードの最適化を試みます。特定の複雑な操作(例: 64ビット除算を32ビットアーキテクチャで行う場合)に対して、コンパイラは直接的なCPU命令のシーケンスを生成する代わりに、ランタイムライブラリ内のヘルパー関数(または組み込み関数/intrinsic)を呼び出すコードを生成することがあります。これは、その操作が複雑で、コンパイラが直接効率的な命令シーケンスを生成するのが難しい場合や、特定のアーキテクチャ固有の最適化をヘルパー関数にカプセル化する場合に発生します。
#pragma textflag 7
#pragma
ディレクティブは、コンパイラに対する特別な指示です。#pragma textflag 7
は、GCC(GNU Compiler Collection)のようなコンパイラで使用される可能性のある、特定のセクション属性や最適化ヒントを指定するものです。textflag 7
は、関数を「リーフ関数」(他の関数を呼び出さない関数)としてマークしたり、コードセグメント内の特定の場所に配置したりするために使用されることがあります。リーフ関数としてマークされると、コンパイラは関数呼び出し規約を簡素化したり、レジスタの保存/復元を最小限に抑えたりするなど、より積極的な最適化を適用できる場合があります。これは、関数呼び出しのオーバーヘッドを削減し、パフォーマンスを向上させるのに役立ちます。
技術的詳細
このコミットは、Goランタイムの2つのファイル、src/pkg/runtime/runtime.c
とsrc/pkg/runtime/vlrt_arm.c
にわたる変更を含んでいます。
src/pkg/runtime/runtime.c
におけるruntime·timediv
の変更
runtime·timediv
関数は、64ビット整数v
を32ビット整数div
で除算し、商と余りを計算するGoランタイムの内部関数です。この関数は、ビットシフトと減算を繰り返すことで除算を実装しています。
元のコード:
v -= (int64)bit*div;
変更後のコード:
v = v - (int64)bit*div;
この変更は、一見すると単なる構文上の違い(複合代入演算子-=
から通常の代入演算子=
への変更)に見えます。しかし、この変更の目的は、コンパイラが生成するコードに影響を与えることです。特定のコンパイラ(特に古いバージョンや特定の最適化レベル)では、v -= expr;
のような複合代入演算子が、v = v - expr;
とは異なる内部的な処理パスをトリガーし、場合によっては_vasop
のようなランタイムヘルパー関数への呼び出しを生成することがありました。
このコミットの意図は、v = v - (int64)bit*div;
と明示的に記述することで、コンパイラがこの減算操作を直接インラインで処理し、_vasop
ヘルパーへの不要な呼び出しを回避するように誘導することです。これにより、関数呼び出しのオーバーヘッドが削減され、timediv
関数の実行効率が向上します。
src/pkg/runtime/vlrt_arm.c
における_addv
と_subv
の変更
vlrt_arm.c
ファイルは、ARMアーキテクチャに特化した64ビット整数(Vlong
構造体で表現)の算術演算ヘルパー関数を含んでいます。
_addv
関数の変更
_addv
関数は、2つの64ビット整数a
とb
を加算し、結果をr
に格納します。
元のコードでは、lo
とhi
という一時変数を使用して加算を行っていました。
void _addv(Vlong *r, Vlong a, Vlong b) {
ulong lo, hi;
lo = a.lo + b.lo;
hi = a.hi + b.hi;
if(lo < a.lo) // Check for carry
hi++;
r->lo = lo;
r->hi = hi;
}
変更後のコードでは、一時変数を削除し、直接r->lo
とr->hi
に結果を代入しています。
void _addv(Vlong *r, Vlong a, Vlong b) {
r->lo = a.lo + b.lo;
r->hi = a.hi + b.hi;
if(r->lo < a.lo) // Check for carry
r->hi++;
}
この変更は、一時変数の割り当てとコピーを削減することで、生成されるアセンブリコードをわずかに簡素化し、レジスタの使用効率を向上させるマイクロ最適化です。これにより、関数全体の実行効率が向上する可能性があります。
_subv
関数の変更と#pragma textflag 7
の追加
_subv
関数は、2つの64ビット整数a
からb
を減算し、結果をr
に格納します。
_addv
と同様に、元のコードでは一時変数lo
とhi
を使用して減算を行っていました。
void _subv(Vlong *r, Vlong a, Vlong b) {
ulong lo, hi;
lo = a.lo - b.lo;
hi = a.hi - b.hi;
if(lo > a.lo) // Check for borrow
hi--;
r->lo = lo;
r->hi = hi;
}
変更後のコードでは、一時変数を削除し、直接r->lo
とr->hi
に結果を代入しています。
#pragma textflag 7
void _subv(Vlong *r, Vlong a, Vlong b) {
r->lo = a.lo - b.lo;
r->hi = a.hi - b.hi;
if(r->lo > a.lo) // Check for borrow
r->hi--;
}
さらに重要な変更は、_subv
関数の定義の前に#pragma textflag 7
が追加されたことです。このプラグマは、コンパイラに対して_subv
関数を特定の最適化属性で処理するように指示します。前述の通り、textflag 7
は関数をリーフ関数としてマークしたり、特定のコードセグメントに配置したりするために使用されることが多く、これによりコンパイラはより積極的なインライン化やレジスタ最適化を適用できるようになります。
コミットメッセージの「mark _subv okay」という記述は、この#pragma textflag 7
の追加によって、_subv
関数が特定のコンパイラ最適化パスやランタイムの要件を満たすようになり、その結果として「問題なく」利用できるようになったことを示唆しています。これは、_subv
が他の重要なランタイムコンポーネントから効率的に呼び出されることを保証するための措置であると考えられます。
コアとなるコードの変更箇所
src/pkg/runtime/runtime.c
--- a/src/pkg/runtime/runtime.c
+++ b/src/pkg/runtime/runtime.c
@@ -429,7 +429,7 @@ runtime·timediv(int64 v, int32 div, int32 *rem)
res = 0;
for(bit = 0x40000000; bit != 0; bit >>= 1) {
if(v >= (int64)bit*div) {
- v -= (int64)bit*div;
+ v = v - (int64)bit*div;
res += bit;
}
}
src/pkg/runtime/vlrt_arm.c
--- a/src/pkg/runtime/vlrt_arm.c
+++ b/src/pkg/runtime/vlrt_arm.c
@@ -66,27 +66,20 @@ void runtime·abort(void);
void
_addv(Vlong *r, Vlong a, Vlong b)
{
- ulong lo, hi;
-
- lo = a.lo + b.lo;
- hi = a.hi + b.hi;
- if(lo < a.lo)
- hi++;
- r->lo = lo;
- r->hi = hi;
+ r->lo = a.lo + b.lo;
+ r->hi = a.hi + b.hi;
+ if(r->lo < a.lo)
+ r->hi++;
}
+#pragma textflag 7
void
_subv(Vlong *r, Vlong a, Vlong b)
{
- ulong lo, hi;
-
- lo = a.lo - b.lo;
- hi = a.hi - b.hi;
- if(lo > a.lo)
- hi--;
- r->lo = lo;
- r->hi = hi;
+ r->lo = a.lo - b.lo;
+ r->hi = a.hi - b.hi;
+ if(r->lo > a.lo)
+ r->hi--;
}
void
コアとなるコードの解説
runtime.c
の変更点
runtime·timediv
関数内のv -= (int64)bit*div;
という行がv = v - (int64)bit*div;
に変更されました。この変更は、コンパイラがv -= ...
という複合代入演算子を処理する際に、特定のアーキテクチャやコンパイラのバージョンで、_vasop
のようなランタイムヘルパー関数を呼び出すコードを生成する可能性があったためです。明示的にv = v - ...
と記述することで、コンパイラがこの減算操作をより直接的に、かつインラインで処理するよう促し、ヘルパー関数の呼び出しによるオーバーヘッドを回避します。これは、Goランタイムの低レベルな算術演算のパフォーマンスを向上させるための、非常に細かい最適化です。
vlrt_arm.c
の変更点
_addv
と_subv
の共通変更
両関数において、ulong lo, hi;
という一時変数の宣言と、それらへの代入、そして最終的なr->lo = lo; r->hi = hi;
という代入が削除されました。代わりに、演算結果が直接r->lo
とr->hi
に代入されるようになりました。
例: lo = a.lo + b.lo;
と r->lo = lo;
が r->lo = a.lo + b.lo;
に。
この変更は、一時変数の使用をなくすことで、コンパイラが生成するアセンブリコードを簡素化し、スタックの使用量やレジスタの圧迫を減らすことを目的としたマイクロ最適化です。これにより、これらの64ビット算術ヘルパー関数の実行効率がわずかに向上します。
_subv
への#pragma textflag 7
の追加
_subv
関数の定義の直前に#pragma textflag 7
が追加されました。このプラグマは、コンパイラに対して_subv
関数を特定の属性でコンパイルするように指示します。具体的には、このプラグマは関数を「リーフ関数」(他の関数を呼び出さない関数)としてマークしたり、特定のコードセグメントに配置したりするために使用されることがあります。リーフ関数としてマークされることで、コンパイラは関数呼び出しのオーバーヘッドを削減するためのより積極的な最適化(例: レジスタの保存/復元を最小限に抑える)を適用できるようになります。コミットメッセージの「mark _subv okay」は、このプラグマによって_subv
が特定の最適化パスやランタイムの要件を満たし、効率的に利用できるようになったことを示唆しています。
これらの変更は全体として、GoランタイムがARMアーキテクチャ上で64ビット整数演算をより効率的に、かつ予測可能な方法で実行できるようにするための、低レベルなパフォーマンス最適化の一環です。
関連リンク
- Go言語の公式ウェブサイト: https://golang.org/
- Goのソースコードリポジトリ (GitHub): https://github.com/golang/go
- Goのコードレビューシステム (Gerrit): https://go-review.googlesource.com/
- このコミットのGerritリンク: https://golang.org/cl/12028046
参考にした情報源リンク
- Goのソースコード (特に
src/pkg/runtime/
ディレクトリ) - GCCのプラグマに関するドキュメント (一般的な
#pragma
の動作理解のため) - ARMアーキテクチャの仕様書 (32ビットシステムでの64ビット演算の理解のため)
- Goのコミット履歴と関連する議論 (Gerritやメーリングリスト)