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

[インデックス 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ビット整数(int64uint64)を直接扱うことができません。

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.csrc/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ビット整数abを加算し、結果をrに格納します。 元のコードでは、lohiという一時変数を使用して加算を行っていました。

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->lor->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と同様に、元のコードでは一時変数lohiを使用して減算を行っていました。

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->lor->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->lor->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のソースコード (特にsrc/pkg/runtime/ディレクトリ)
  • GCCのプラグマに関するドキュメント (一般的な#pragmaの動作理解のため)
  • ARMアーキテクチャの仕様書 (32ビットシステムでの64ビット演算の理解のため)
  • Goのコミット履歴と関連する議論 (Gerritやメーリングリスト)