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

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

このコミットは、GoランタイムのARMアーキテクチャ向けビルドにおける問題を修正するものです。具体的には、src/pkg/runtime/vlrt_arm.c ファイル内の特定の関数にスタック分割を行わないよう指示する#pragma textflag 7を追加し、_lshv関数のロジックを修正しています。これにより、ARM環境でのGoプログラムの安定した動作を保証します。

コミット

commit 184b02ea9f057d0932e7182b14956568d5a10cfd
Author: Rémy Oudompheng <oudomphe@phare.normalesup.org>
Date:   Thu Aug 1 07:48:21 2013 +0200

    runtime: fix arm build.
    
    More functions needs to be marked as no stack split.
    
    R=golang-dev, rsc
    CC=golang-dev
    https://golang.org/cl/11963044

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

https://github.com/golang/go/commit/184b02ea9f057d0932e7182b14956568d5a10cfd

元コミット内容

runtime: fix arm build.

More functions needs to be marked as no stack split.

変更の背景

このコミットが行われた2013年当時、Go言語のランタイムは「スタック分割 (segmented stacks)」というメカニズムを使用してゴルーチンのスタックを管理していました。これは、ゴルーチンに小さなスタックを割り当て、必要に応じて新しいスタックセグメントを動的にリンクすることでスタックを拡張する方式でした。このアプローチは、多数の軽量なゴルーチンを効率的に扱うことを目的としていました。

しかし、このスタック分割の仕組みは、特に低レベルのランタイム関数において問題を引き起こすことがありました。スタックチェックのプリアンブルが挿入されることで、スタックの限界が誤って計算されたり、特定の関数がnosplit(スタック分割を行わない)として正しくマークされていない場合にビルドエラーやランタイムエラーが発生する可能性がありました。ARMアーキテクチャのような特定の環境では、これらの問題が顕著に現れることがありました。

このコミットは、ARMビルドが失敗する問題を解決するために、さらに多くの関数を「スタック分割なし(no stack split)」としてマークする必要があるという認識に基づいて行われました。これにより、これらの低レベル関数が固定された小さなスタック空間内で安全に動作し、スタック関連のビルドエラーやランタイムエラーを防ぐことが目的です。

前提知識の解説

Goのスタック管理(スタック分割とスタックコピー)

Go言語のゴルーチンは、非常に軽量な並行処理の単位です。Goのランタイムは、これらのゴルーチンのスタックを効率的に管理する必要があります。

  • スタック分割 (Segmented Stacks): 2014年後半にGo 1.4がリリースされるまで、Goのランタイムは「スタック分割」という方式を採用していました。これは、ゴルーチンに最初は小さなスタックを割り当て、関数呼び出しによってスタックが不足しそうになると、新しいスタックセグメントを動的に割り当てて既存のスタックにリンクするというものでした。これにより、多数のゴルーチンをメモリ効率よく実行できるという利点がありました。しかし、スタックの成長時にオーバーヘッドが発生したり、スタックトレースが複雑になったり、特定の低レベル関数でスタックチェックが問題を引き起こすなどの課題も抱えていました。
  • スタックコピー (Stack Copying): Go 1.4以降、Goのランタイムは「スタックコピー」モデルに移行しました。これは、スタックが連続したメモリ領域として確保され、スタックが不足しそうになった場合に、より大きな新しいスタック領域を割り当て、既存のスタックの内容を新しい領域にコピーするという方式です。この変更により、スタックの成長時のパフォーマンスが予測可能になり、「ホットスタック分割」の問題が解消されました。

このコミットは、スタック分割モデルが主流だった時期に行われたため、その問題に対処するものです。

NOSPLITtextflag

  • NOSPLIT: Goのコンパイラに対する指示で、特定の関数に対してスタックチェックのプリアンブル(スタックが足りるかを確認し、足りなければスタックを拡張するコード)を挿入しないように命令します。これは、非常に低レベルのランタイム関数やアセンブリで書かれた関数など、スタックの成長を許容しない、あるいは固定された小さなスタック空間内で動作する必要がある関数に適用されます。NOSPLITが指定された関数は、他の関数を呼び出す際にスタックを拡張できないため、スタックオーバーフローを引き起こす可能性があります。
  • textflag: Goのコンパイラやリンカに対するメタデータ(フラグ)を関数やデータオブジェクトに関連付けるための概念です。これらのフラグは、Goの内部ソースファイル(例: src/runtime/textflag.h)で定義されています。
  • #pragma textflag 7: C言語のプリプロセッサディレクティブに似ていますが、Goのコンパイラに対する特別な指示として機能します。textflag 7という値は、複数のフラグのビットマスクの組み合わせです。
    • NOPROF (値 1): プロファイリングを行わない(非推奨)。
    • DUPOK (値 2): リンカがこのシンボルの複数の重複を見つけても問題ない。リンカは重複の中から一つを選択して使用する。
    • NOSPLIT (値 4): スタックチェックのプリアンブルを挿入しない。 したがって、textflag 7NOPROF | DUPOK | NOSPLITを意味します。このコミットの文脈では、特にNOSPLITが重要であり、関数がスタック分割を行わないように指示しています。

Vlong型と_lshv関数

  • Vlong: Goの標準ライブラリや公開APIには存在しない、Goランタイム内部で使用される型です。これは通常、64ビット整数を表現するための内部的な構造体やエイリアスを指します。特に、32ビットアーキテクチャ(ARMv5/v6/v7など)で64ビット整数を扱う際に、lo(下位32ビット)とhi(上位32ビット)の2つの32ビット値で構成される構造体として定義されることが多いです。
  • _lshv: Goランタイム内部の関数で、おそらく「left shift value」の略です。これは、Vlong型(64ビット整数)に対して左シフト演算を行うための低レベルな関数です。Goの内部では、このような低レベルのビット演算がアセンブリ言語やC言語で書かれたファイル(この場合はvlrt_arm.c)で実装され、特定のアーキテクチャ向けに最適化されています。

技術的詳細

このコミットは、GoランタイムのARMアーキテクチャ向けコード、特にsrc/pkg/runtime/vlrt_arm.cファイルに焦点を当てています。このファイルは、ARMプロセッサ上で64ビット整数(Vlong)の演算を効率的に行うための低レベルなルーチンを含んでいます。

_lshv関数の修正

_lshv関数は、64ビット整数abビットだけ左シフトし、結果をrに格納する関数です。元のコードでは、a.loVlongの下位32ビット)を一時変数tに格納してから演算を行っていました。

// 修正前
_lshv(Vlong *r, Vlong a, int b)
{
    ulong t; // 一時変数
    t = a.lo; // a.loをtにコピー
    if(b >= 32) {
        r->lo = 0;
        if(b >= 64) {
            r->hi = 0;
            return;
        }
        r->hi = t << (b-32); // tを使用
        return;
    }
    if(b <= 0) {
        r->lo = t; // tを使用
        r->hi = a.hi;
        return;
    }
    r->lo = t << b; // tを使用
    r->hi = (t >> (32-b)) | (a.hi << b); // tを使用
}

この修正では、一時変数tを削除し、直接a.loを使用するように変更されています。

// 修正後
_lshv(Vlong *r, Vlong a, int b)
{
    // ulong t; // 削除
    // t = a.lo; // 削除
    if(b >= 32) {
        r->lo = 0;
        if(b >= 64) {
            r->hi = 0;
            return;
        }
        r->hi = a.lo << (b-32); // a.loを直接使用
        return;
    }
    if(b <= 0) {
        r->lo = a.lo; // a.loを直接使用
        r->hi = a.hi;
        return;
    }
    r->lo = a.lo << b; // a.loを直接使用
    r->hi = (a.lo >> (32-b)) | (a.hi << b); // a.loを直接使用
}

この変更は、コードの簡素化と、おそらくはわずかなパフォーマンス改善を目的としています。一時変数を介さずに直接元の値を使用することで、コンパイラがより効率的なコードを生成できる可能性があります。

#pragma textflag 7の追加

このコミットの主要な変更点は、_v2si関数と_gev関数の定義の直前に#pragma textflag 7を追加したことです。

// _v2si関数の前に追加
+#pragma textflag 7
long
_v2si(Vlong rv)
{
    // ...
}

// _gev関数の前に追加
+#pragma textflag 7
int
_gev(Vlong lv, Vlong rv)
{
    // ...
}

前述の通り、textflag 7NOPROF | DUPOK | NOSPLITの組み合わせです。この文脈では、特にNOSPLITフラグが重要です。

  • _v2si: Vlong型(64ビット整数)を32ビット符号付き整数に変換する関数です。
  • _gev: 2つのVlong型(64ビット整数)を比較し、最初の値が2番目の値以上であるかどうかを判定する関数です。

これらの関数は、Goランタイムの非常に低レベルな部分であり、スタックの成長を伴う通常の関数呼び出しのセマンティクスとは異なる動作が期待されます。NOSPLITフラグを付けることで、コンパイラはこれらの関数にスタックチェックのプリアンブルを挿入しなくなります。これにより、これらの関数が固定された小さなスタック空間内で安全に動作することが保証され、特にARMのようなリソースが限られた環境でのビルドエラーやランタイムエラーを防ぐことができます。

この修正は、Goランタイムが特定のアーキテクチャ(この場合はARM)で正しく動作するために、低レベルの関数がスタック管理の規則に厳密に従う必要があることを示しています。

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

--- a/src/pkg/runtime/vlrt_arm.c
+++ b/src/pkg/runtime/vlrt_arm.c
@@ -425,9 +425,6 @@ _rshlv(Vlong *r, Vlong a, int b)
 void
 _lshv(Vlong *r, Vlong a, int b)
 {
-	ulong t;
-
-	t = a.lo;
 	if(b >= 32) {
 		r->lo = 0;
 		if(b >= 64) {
 			r->hi = 0;
 			return;
 		}
-		r->hi = t << (b-32);
+		r->hi = a.lo << (b-32);
 		return;
 	}
 	if(b <= 0) {
-		r->lo = t;
+		r->lo = a.lo;
 		r->hi = a.hi;
 		return;
 	}
-	r->lo = t << b;
-	r->hi = (t >> (32-b)) | (a.hi << b);
+	r->lo = a.lo << b;
+	r->hi = (a.lo >> (32-b)) | (a.hi << b);
 }
 
 void
@@ -722,6 +719,7 @@ _v2ul(Vlong rv)
 	return rv.lo;
 }
 
+#pragma textflag 7
 long
 _v2si(Vlong rv)
 {
@@ -775,6 +773,7 @@ _gtv(Vlong lv, Vlong rv)
 		(lv.hi == rv.hi && lv.lo > rv.lo);
 }
 
+#pragma textflag 7
 int
 _gev(Vlong lv, Vlong rv)
 {

コアとなるコードの解説

_lshv関数の変更

  • ulong t;t = a.lo; の削除: 一時変数tの宣言と、a.lotにコピーする行が削除されました。
  • tからa.loへの直接参照への変更: r->hi = t << (b-32);r->hi = a.lo << (b-32); に、 r->lo = t;r->lo = a.lo; に、 r->lo = t << b;r->lo = a.lo << b; に、 r->hi = (t >> (32-b)) | (a.hi << b);r->hi = (a.lo >> (32-b)) | (a.hi << b); にそれぞれ変更されました。 この変更は、コードの冗長性を排除し、a.loの値を直接使用することで、より簡潔で効率的なコードパスを実現します。コンパイラは、一時変数を介するよりも直接レジスタ間で操作を行う方が効率的であると判断する可能性があります。

#pragma textflag 7の追加

  • _v2si関数の前: long _v2si(Vlong rv) の定義の直前に #pragma textflag 7 が追加されました。これにより、_v2si関数はNOSPLIT(スタック分割なし)としてマークされます。この関数は64ビット値を32ビット符号付き整数に変換する低レベルな操作を行うため、スタックチェックのオーバーヘッドを避け、固定されたスタック空間で動作することが望ましいです。
  • _gev関数の前: int _gev(Vlong lv, Vlong rv) の定義の直前に #pragma textflag 7 が追加されました。同様に、_gev関数もNOSPLITとしてマークされます。この関数は2つの64ビット値を比較する低レベルな操作を行うため、スタックチェックを無効にすることで、より予測可能で効率的な実行が可能になります。

これらの#pragma textflag 7の追加は、GoランタイムのARMビルドにおけるスタック関連の問題を解決するための重要なステップです。これにより、これらの低レベル関数がスタックの制約内で正しく動作し、全体的な安定性が向上します。

関連リンク

参考にした情報源リンク