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

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

このコミットは、Go言語のコンパイラ (cmd/6g) とランタイム (src/pkg/runtime) における amd64p32 アーキテクチャ向けのメモリ配置(アライメント)に関する修正を扱っています。特に、ハッシュマップのルックアップ処理、パニックからの回復処理、およびランタイムのプリント関数におけるアライメントの問題を解決することを目的としています。

コミット

commit f210fd1fa905ad381c8cb358ed7c004ec582f90f
Author: Rémy Oudompheng <oudomphe@phare.normalesup.org>
Date:   Fri Mar 14 19:37:39 2014 +0100

    cmd/6g, runtime: alignment fixes for amd64p32.
    
    LGTM=rsc
    R=rsc, dave, iant, khr
    CC=golang-codereviews
    https://golang.org/cl/75820044

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

https://github.com/golang/go/commit/f210fd1fa905ad381c8cb358ed7c004ec582f90f

元コミット内容

cmd/6g, runtime: alignment fixes for amd64p32.

LGTM=rsc
R=rsc, dave, iant, khr
CC=golang-codereviews
https://golang.org/cl/75820044

変更の背景

このコミットの背景には、amd64p32 という特定のアーキテクチャにおけるメモリのアライメント要件があります。amd64p32 は、64ビットのAMD64(x86-64)命令セットを使用しながらも、ポインタサイズが32ビットであるという特殊な環境を指します。このような環境では、データ構造のメモリ配置が通常の64ビット環境とは異なるため、アライメントの不一致が発生しやすくなります。

Goランタイムは、効率的なメモリ管理とデータアクセスを実現するために、特定のアライメント要件を満たすように設計されています。しかし、amd64p32 のような非標準的なポインタサイズを持つ環境では、既存のアライメントロジックが適切に機能しない可能性がありました。特に、ハッシュマップのキーや値、パニックからの回復時に使用される構造体、およびランタイムのデバッグ出力における引数の処理において、アライメントの問題が顕在化していました。

このコミットは、これらのアライメントの不整合を修正し、amd64p32 環境においてもGoプログラムが正しく、かつ効率的に動作することを保証するために行われました。

前提知識の解説

メモリのアライメント

メモリのアライメントとは、コンピュータのメモリ上でデータが配置される際の、特定の境界への整列を指します。CPUは、特定のバイト境界(例えば、4バイト境界、8バイト境界)に配置されたデータを効率的に読み書きできます。データがその自然なアライメント境界に配置されていない場合(アライメントされていないアクセス)、CPUは追加のサイクルを費やしたり、場合によってはアライメント例外を発生させたりすることがあります。

  • 自然なアライメント: 多くのシステムでは、データ型はそのサイズと同じバイト境界にアライメントされます。例えば、4バイトの整数は4バイト境界に、8バイトのポインタは8バイト境界に配置されます。
  • パディング: 構造体などの複合データ型では、メンバ間のアライメント要件を満たすために、コンパイラが自動的にパディング(空きバイトの挿入)を行うことがあります。
  • ポインタサイズ: ポインタが指すメモリアドレスのサイズ。一般的な64ビットシステムではポインタも64ビット(8バイト)ですが、amd64p32 のように32ビット(4バイト)のポインタを使用する特殊な環境も存在します。

amd64p32

amd64p32 は、x86-64(AMD64)アーキテクチャの命令セットを使用しながら、ポインタのサイズが32ビットに制限されている環境を指します。これは、主にメモリ使用量を削減したい場合や、特定のレガシーシステムとの互換性を維持したい場合に使用されることがあります。

通常の64ビット環境では、uintptr(ポインタを保持できる符号なし整数型)のサイズは8バイトですが、amd64p32 では4バイトになります。この違いが、メモリのアライメント計算やデータ構造のオフセット計算に影響を与え、問題を引き起こす可能性があります。

Goランタイムの内部構造

Goランタイムは、ガベージコレクション、スケジューラ、マップ、チャネルなどの低レベルな機能を提供します。これらの機能はC言語で書かれた部分(src/pkg/runtime 以下)とGo言語で書かれた部分が混在しており、特にパフォーマンスが要求される部分やOSとのインタフェース部分はC言語で実装されています。

  • MapTypeHmapBucket: Goのマップ(map)は、内部的にはハッシュテーブルとして実装されており、MapType はマップの型情報、Hmap はハッシュマップのヘッダ、Bucket はハッシュテーブルのバケットを表します。
  • Eface: Goの空のインタフェース(interface{})は、内部的には Eface 構造体として表現されます。これは、型情報 (_type) とデータポインタ (data) の2つのフィールドを持ちます。
  • FLUSH マクロ: このコミットで削除されている FLUSH マクロは、おそらくコンパイラや最適化器に対して、特定の変数の値がメモリに書き込まれたことを保証するためのものでした。しかし、アライメントの問題を解決する新しいアプローチでは不要になったか、あるいは別の方法で同等の効果が得られるようになったと考えられます。

技術的詳細

このコミットの主要な技術的詳細は、amd64p32 環境におけるポインタサイズとアライメントの不一致を解消することにあります。

  1. cmd/6g/ggen.c の変更:

    • defframe 関数内で、スタックフレームのサイズを計算する際に widthptr の代わりに widthreg を使用するように変更されています。
    • widthptr はポインタの幅(sizeof(uintptr))を表し、amd64p32 では4バイトです。
    • widthreg はレジスタの幅(通常は64ビットアーキテクチャでは8バイト)を表します。
    • この変更は、スタックフレームのアライメントをポインタサイズではなく、レジスタサイズに合わせることで、より広いアライメント要件を持つデータ型が正しく配置されるようにするためのものです。これにより、amd64p32 環境でもスタック上のデータアクセスが効率的かつ安全になります。
  2. src/pkg/runtime/hashmap_fast.c の変更:

    • HASH_LOOKUP1 および HASH_LOOKUP2 関数(マップのルックアップ処理)のシグネチャが変更されています。
    • 以前は byte *valuebool res といった出力引数を直接受け取っていましたが、これらが GoOutput base, ... という可変引数リストに置き換えられています。
    • 関数内部では、valueptr = (byte**)&base;okptr = (bool*)(valueptr+1); のように、base 引数から出力ポインタを計算しています。
    • これにより、出力値が直接引数として渡されるのではなく、呼び出し元が提供するメモリ領域へのポインタを介して書き込まれるようになります。
    • FLUSH(&value);FLUSH(&res); といった FLUSH マクロの呼び出しが削除され、代わりに *valueptr = ...;*okptr = ...; のように直接ポインタを介して値が代入されています。
    • この変更は、amd64p32 環境で出力引数のアライメントが正しく行われない問題を回避するためのものです。可変引数リストとポインタ演算を使用することで、コンパイラによるアライメントの自動調整に依存せず、ランタイムが明示的にメモリ配置を制御できるようになります。
  3. src/pkg/runtime/panic.c の変更:

    • runtime·recover 関数(パニックからの回復処理)のシグネチャが Eface ret から GoOutput retbase, ... に変更されています。
    • hashmap_fast.c と同様に、ret = (Eface*)&retbase; のように base 引数から Eface 構造体へのポインタを取得し、*ret = p->arg;ret->type = nil; ret->data = nil; のようにポインタを介して値を設定しています。
    • ここでも FLUSH(&ret); が削除されています。
    • この変更も、amd64p32 環境における Eface 構造体のアライメント問題を解決し、パニックからの回復処理が安定して動作するようにするためのものです。
  4. src/pkg/runtime/print.c の変更:

    • vprintf 関数内で、'U', 'X', 'f', 'C' フォーマット指定子に対応する引数のアライメント計算が sizeof(uintptr) から sizeof(uintreg) に変更されています。
    • uintptr はポインタサイズ(amd64p32 では4バイト)、uintreg はレジスタサイズ(64ビットアーキテクチャでは8バイト)です。
    • この変更は、ランタイムのデバッグ出力関数が、amd64p32 環境においても引数を正しくアライメントして読み取れるようにするためのものです。特に浮動小数点数 ('f') や大きな整数 ('U', 'X')、複合型 ('C') の引数は、より厳密なアライメント要件を持つことが多いため、uintreg に合わせることで安全性が向上します。

全体として、このコミットは amd64p32 という特殊な環境において、Goランタイムがメモリを正しく扱い、アライメントの不整合によるクラッシュや不正な動作を防ぐための重要な修正です。特に、出力引数を直接渡すのではなく、ポインタを介して値を書き込むように変更することで、アライメントの制御をより細かく行えるようになっています。

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

src/cmd/6g/ggen.c

--- a/src/cmd/6g/ggen.c
+++ b/src/cmd/6g/ggen.c
@@ -22,7 +22,7 @@ defframe(Prog *ptxt)
 
 	// fill in final stack size
 	ptxt->to.offset <<= 32;
-	frame = rnd(stksize+maxarg, widthptr);
+	frame = rnd(stksize+maxarg, widthreg);
 	ptxt->to.offset |= frame;
 	
 	// insert code to contain ambiguously live variables

src/pkg/runtime/hashmap_fast.c

--- a/src/pkg/runtime/hashmap_fast.c
+++ b/src/pkg/runtime/hashmap_fast.c
@@ -12,15 +12,16 @@
 
 #pragma textflag NOSPLIT
 void
-HASH_LOOKUP1(MapType *t, Hmap *h, KEYTYPE key, byte *value)
+HASH_LOOKUP1(MapType *t, Hmap *h, KEYTYPE key, GoOutput base, ...)
 {
 	uintptr bucket, i;
 	Bucket *b;
 	KEYTYPE *k;
-	byte *v;
+	byte *v, **valueptr;
 	uint8 top;
 	int8 keymaybe;
 
+	valueptr = (byte**)&base;
 	if(debug) {
 		runtime·prints("runtime.mapaccess1_fastXXX: map=");
 		runtime·printpointer(h);
@@ -29,8 +30,7 @@ HASH_LOOKUP1(MapType *t, Hmap *h, KEYTYPE key, byte *value)
 		runtime·prints("\n");
 	}
 	if(h == nil || h->count == 0) {
-		value = t->elem->zero;
-		FLUSH(&value);
+		*valueptr = t->elem->zero;
 		return;
 	}
 	if(raceenabled)
@@ -48,8 +48,7 @@ HASH_LOOKUP1(MapType *t, Hmap *h, KEYTYPE key, byte *value)
 			if(QUICK_NE(key, *k))
 				continue;
 			if(QUICK_EQ(key, *k) || SLOW_EQ(key, *k)) {
-				value = v;
-				FLUSH(&value);
+				*valueptr = v;
 				return;
 			}
 		}
@@ -61,8 +60,7 @@ HASH_LOOKUP1(MapType *t, Hmap *h, KEYTYPE key, byte *value)
 			if(QUICK_NE(key, *k))
 				continue;
 			if(QUICK_EQ(key, *k)) {
-				value = v;
-				FLUSH(&value);
+				*valueptr = v;
 				return;
 			}
 			if(MAYBE_EQ(key, *k)) {
@@ -80,8 +78,7 @@ HASH_LOOKUP1(MapType *t, Hmap *h, KEYTYPE key, byte *value)
 			if(keymaybe >= 0) {
 				k = (KEYTYPE*)b->data + keymaybe;
 				if(SLOW_EQ(key, *k)) {
-					value = (byte*)((KEYTYPE*)b->data + BUCKETSIZE) + keymaybe * h->valuesize;
-					FLUSH(&value);
+					*valueptr = (byte*)((KEYTYPE*)b->data + BUCKETSIZE) + keymaybe * h->valuesize;
 					return;
 				}
 			}
@@ -110,29 +107,30 @@ dohash:
 			if(QUICK_NE(key, *k))
 				continue;
 			if(QUICK_EQ(key, *k) || SLOW_EQ(key, *k)) {
-				value = v;
-				FLUSH(&value);
+				*valueptr = v;
 				return;
 			}
 		}
 		b = b->overflow;
 	} while(b != nil);
-	value = t->elem->zero;
-	FLUSH(&value);
+	*valueptr = t->elem->zero;
 }
 
 #pragma textflag NOSPLIT
 void
-HASH_LOOKUP2(MapType *t, Hmap *h, KEYTYPE key, byte *value, bool res)
+HASH_LOOKUP2(MapType *t, Hmap *h, KEYTYPE key, GoOutput base, ...)
 {
 	uintptr bucket, i;
 	Bucket *b;
 	KEYTYPE *k;
-	byte *v;
+	byte *v, **valueptr;
 	uint8 top;
 	int8 keymaybe;
+	bool *okptr;
 
+	valueptr = (byte**)&base;
+	okptr = (bool*)(valueptr+1);
 	if(debug) {
 		runtime·prints("runtime.mapaccess2_fastXXX: map=");
 		runtime·printpointer(h);
@@ -141,10 +139,8 @@ HASH_LOOKUP2(MapType *t, Hmap *h, KEYTYPE key, byte *value, bool res)
 		runtime·prints("\n");
 	}
 	if(h == nil || h->count == 0) {
-		value = t->elem->zero;
-		res = false;
-		FLUSH(&value);
-		FLUSH(&res);
+		*valueptr = t->elem->zero;
+		*okptr = false;
 		return;
 	}
 	if(raceenabled)
@@ -162,10 +158,8 @@ HASH_LOOKUP2(MapType *t, Hmap *h, KEYTYPE key, byte *value, bool res)
 			if(QUICK_NE(key, *k))
 				continue;
 			if(QUICK_EQ(key, *k) || SLOW_EQ(key, *k)) {
-				value = v;
-				res = true;
-				FLUSH(&value);
-				FLUSH(&res);
+				*valueptr = v;
+				*okptr = true;
 				return;
 			}
 		}
@@ -177,10 +171,8 @@ HASH_LOOKUP2(MapType *t, Hmap *h, KEYTYPE key, byte *value, bool res)
 			if(QUICK_NE(key, *k))
 				continue;
 			if(QUICK_EQ(key, *k)) {
-				value = v;
-				res = true;
-				FLUSH(&value);
-				FLUSH(&res);
+				*valueptr = v;
+				*okptr = true;
 				return;
 			}
 			if(MAYBE_EQ(key, *k)) {
@@ -198,10 +190,8 @@ HASH_LOOKUP2(MapType *t, Hmap *h, KEYTYPE key, byte *value, bool res)
 			if(keymaybe >= 0) {
 				k = (KEYTYPE*)b->data + keymaybe;
 				if(SLOW_EQ(key, *k)) {
-					value = (byte*)((KEYTYPE*)b->data + BUCKETSIZE) + keymaybe * h->valuesize;
-					res = true;
-					FLUSH(&value);
-					FLUSH(&res);
+					*valueptr = (byte*)((KEYTYPE*)b->data + BUCKETSIZE) + keymaybe * h->valuesize;
+					*okptr = true;
 					return;
 				}
 			}
@@ -230,18 +220,14 @@ dohash:
 			if(QUICK_NE(key, *k))
 				continue;
 			if(QUICK_EQ(key, *k) || SLOW_EQ(key, *k)) {
-				value = v;
-				res = true;
-				FLUSH(&value);
-				FLUSH(&res);
+				*valueptr = v;
+				*okptr = true;
 				return;
 			}
 		}
 		b = b->overflow;
 	} while(b != nil);
-	value = t->elem->zero;
-	res = false;
-	FLUSH(&value);
-	FLUSH(&res);
+	*valueptr = t->elem->zero;
+	*okptr = false;
 }

src/pkg/runtime/panic.c

--- a/src/pkg/runtime/panic.c
+++ b/src/pkg/runtime/panic.c
@@ -353,10 +353,11 @@ runtime·unwindstack(G *gp, byte *sp)
 // find the stack segment of its caller.
 #pragma textflag NOSPLIT
 void
-runtime·recover(byte *argp, Eface ret)
+runtime·recover(byte *argp, GoOutput retbase, ...)
 {
 	Panic *p;
 	Stktop *top;
+	Eface *ret;
 
 	// Must be an unrecovered panic in progress.
 	// Must be on a stack segment created for a deferred call during a panic.
@@ -367,16 +368,16 @@ runtime·recover(byte *argp, Eface ret)
 	// do not count as official calls to adjust what we consider the top frame
 	// while they are active on the stack. The linker emits adjustments of
 	// g->panicwrap in the prologue and epilogue of functions marked as wrappers.
+\tret = (Eface*)&retbase;
 	top = (Stktop*)g->stackbase;
 	p = g->panic;
 	if(p != nil && !p->recovered && top->panic && argp == (byte*)top - top->argsize - g->panicwrap) {
 		p->recovered = 1;
-\t\tret = p->arg;
+\t\t*ret = p->arg;
 	} else {
-\t\tret.type = nil;\n-\t\tret.data = nil;
+\t\tret->type = nil;\n+\t\tret->data = nil;
 	}\n-\tFLUSH(&ret);
 }
 
 void

src/pkg/runtime/print.c

--- a/src/pkg/runtime/print.c
+++ b/src/pkg/runtime/print.c
@@ -115,11 +115,11 @@ vprintf(int8 *s, byte *base)
 		case 'U':
 		case 'X':
 		case 'f':
-			arg = ROUND(arg, sizeof(uintptr));
+			arg = ROUND(arg, sizeof(uintreg));
 			siz = 8;
 			break;
 		case 'C':
-			arg = ROUND(arg, sizeof(uintptr));
+			arg = ROUND(arg, sizeof(uintreg));
 			siz = 16;
 			break;
 		case 'p':	// pointer-sized

コアとなるコードの解説

src/cmd/6g/ggen.c の変更

frame = rnd(stksize+maxarg, widthptr); から frame = rnd(stksize+maxarg, widthreg); への変更は、スタックフレームのサイズを計算する際のアライメント基準を変更しています。

  • rnd(val, align)valalign の倍数に切り上げる関数です。
  • stksize は現在のスタックフレームのサイズ、maxarg は関数呼び出し時に引数として渡される可能性のある最大のサイズです。
  • widthptr はポインタのサイズ(sizeof(uintptr))で、amd64p32 では4バイトです。
  • widthreg はレジスタのサイズ(通常は64ビットアーキテクチャでは8バイト)です。

この変更により、スタックフレーム全体が8バイト境界にアライメントされるようになります。これは、64ビットレジスタを使用するCPUが、8バイト境界にアライメントされたデータに対してより効率的にアクセスできるためです。amd64p32 環境ではポインタが4バイトであっても、CPUのレジスタは64ビット(8バイト)であるため、レジスタサイズに合わせたアライメントがパフォーマンスと安定性の向上に寄与します。

src/pkg/runtime/hashmap_fast.c および src/pkg/runtime/panic.c の変更

これらのファイルにおける最も重要な変更は、関数の出力引数の渡し方です。

  • 変更前: HASH_LOOKUP1(..., byte *value)runtime·recover(..., Eface ret) のように、出力値を直接ポインタや構造体として引数に受け取っていました。そして、FLUSH(&value); のように FLUSH マクロを使って値を書き込んでいました。
  • 変更後: HASH_LOOKUP1(..., GoOutput base, ...)runtime·recover(..., GoOutput retbase, ...) のように、GoOutput base という特殊な引数と可変引数リスト (...) を使用するようになりました。関数内部では、valueptr = (byte**)&base;ret = (Eface*)&retbase; のように、base 引数のアドレスを基点として、出力値へのポインタを計算しています。そして、*valueptr = ...;*ret = ...; のように、この計算されたポインタを介して値が書き込まれます。

このアプローチの利点は以下の通りです。

  1. アライメントの制御: amd64p32 のような環境では、コンパイラが自動的に行う引数のアライメントが期待通りにならない場合があります。GoOutput base を使用し、そこから手動でポインタを計算することで、ランタイムがメモリ上の出力値の正確な位置とアライメントをより細かく制御できるようになります。これにより、アライメントの不整合によるクラッシュやデータ破損を防ぎます。
  2. FLUSH マクロの削除: FLUSH マクロは、コンパイラの最適化によって変数の書き込みが遅延されるのを防ぐためのものであった可能性があります。しかし、出力値をポインタを介して直接メモリに書き込む新しい方法では、このマクロが不要になったか、あるいは別の低レベルなメカニズムによって同等の効果が得られるようになったと考えられます。これにより、コードが簡潔になり、オーバーヘッドが削減される可能性があります。

src/pkg/runtime/print.c の変更

arg = ROUND(arg, sizeof(uintptr)); から arg = ROUND(arg, sizeof(uintreg)); への変更は、vprintf 関数が可変引数リストから引数を読み取る際のアライメント基準を修正しています。

  • vprintf は、C言語の printf のような機能を提供するランタイム内部の関数です。
  • ROUND(arg, align) は、arg の値を align の倍数に切り上げることで、次の引数が正しいアライメントで読み取れるようにします。
  • uintptr はポインタサイズ(amd64p32 では4バイト)、uintreg はレジスタサイズ(8バイト)です。

この変更は、特に浮動小数点数や大きな整数など、8バイト境界にアライメントされるべき引数が、amd64p32 環境で4バイト境界にアライメントされてしまう問題を解決します。これにより、vprintf が引数を正しく読み取り、デバッグ出力が正確に行われるようになります。

これらの変更は、amd64p32 という特定のアーキテクチャにおけるメモリのアライメント要件にGoランタイムが適応するための、低レベルかつ重要な修正です。これにより、Goプログラムがより多くの環境で安定して動作することが保証されます。

関連リンク

参考にした情報源リンク

  • Go言語のソースコード
  • Go言語のドキュメント
  • メモリのアライメントに関する一般的なコンピュータサイエンスの知識
  • x86-64アーキテクチャに関する情報
  • Go言語のランタイムに関する技術記事や解説(一般的な知識として)