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

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

このコミットは、Go言語のコンパイラ(src/cmd/gc/walk.c)と、初期のbignumライブラリ(src/lib/bignum.goおよびsrc/lib/bignum_test.go)における重要なバグ修正と診断機能の追加を含んでいます。主な目的は、特定のコードパターンにおけるポインタの誤用を防ぐ診断の強化と、bignumライブラリの計算における正確性の問題を解決することです。

コミット

  • コミットハッシュ: 85815fe0ad000bc57366cd80057abe51da154ad3
  • Author: Ken Thompson ken@golang.org
  • Date: Fri Dec 26 14:42:20 2008 -0800

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

https://github.com/golang/go/commit/85815fe0ad000bc57366cd80057abe51da154ad3

元コミット内容

diagnostic to catch pointer to
rvalue promoted to method receiver.
fixes to bignum that failed.

R=r
OCL=21827
CL=21827

変更の背景

このコミットは、Go言語の初期開発段階における2つの異なる問題に対処しています。

  1. rvalueがメソッドレシーバに昇格される際のポインタ診断の強化: Go言語では、ポインタレシーバを持つメソッドを呼び出す際、呼び出し元の値がアドレス可能(lvalue)であれば、コンパイラが自動的にそのアドレスを取得してメソッドに渡す「昇格(promotion)」という便利な機能があります。しかし、一時的な値(rvalue)はメモリ上の安定したアドレスを持たないため、本来はポインタレシーバを持つメソッドを直接呼び出すことはできません。このコミット以前のコンパイラには、このrvalueに対するポインタレシーバの誤用を適切に診断できないケースが存在したと考えられます。この変更は、そのような潜在的なバグを早期に発見するための診断機能を追加することを目的としています。

  2. bignumライブラリの計算失敗の修正: bignum(任意精度演算)ライブラリは、非常に大きな整数や有理数を扱うために使用されます。このようなライブラリでは、数値の表現や演算のロジックが複雑になりがちで、特にメソッドチェーン(a.Method1().Method2()のように複数のメソッドを連続して呼び出すこと)を使用する際に、予期せぬ動作や計算エラーが発生することがあります。Goのmath/bigパッケージ(このコミット時点ではsrc/lib/bignum.go)のメソッドは、多くの場合、レシーバ自身を結果として変更し、その変更されたレシーバを返す設計になっています。この設計はパフォーマンス最適化のためですが、同じ変数をレシーバとオペランドの両方として使用するような複雑なメソッドチェーンでは、中間結果が意図せず上書きされ、最終的な計算が誤るという問題が発生しやすくなります。このコミットは、bignumライブラリ内の特定の計算が失敗する問題を修正し、特にテストコードにおいて、このメソッドチェーンの落とし穴を回避するための変更を導入しています。

前提知識の解説

rvalueとlvalue

  • lvalue (locator value): メモリ上にアドレスを持ち、そのアドレスを参照できる値です。変数などがこれに該当します。Go言語では、lvalueに対しては&演算子でアドレスを取得できます。 例: var x int = 10; &x
  • rvalue (right value): メモリ上に安定したアドレスを持たない一時的な値です。リテラル(10, "hello")、式の評価結果(a + b)、値を返す関数呼び出しの結果などがこれに該当します。rvalueに対しては&演算子でアドレスを取得することはできません。 例: 10, a + b, someFunction()

Go言語のメソッドレシーバとアドレス可能性

Go言語のメソッドは、レシーバの型によって動作が異なります。

  • 値レシーバ (func (t T) Method()): 値のコピーに対して操作を行います。元の値は変更されません。rvalue、lvalueのどちらに対しても呼び出し可能です。
  • ポインタレシーバ (func (t *T) Method()): レシーバのポインタを通じて元の値にアクセスし、変更することができます。lvalueに対して呼び出す場合、Goコンパイラは自動的にそのアドレスを取得してメソッドに渡します(「昇格」)。しかし、rvalueはアドレスを持たないため、ポインタレシーバを持つメソッドを直接rvalueに対して呼び出すことはできません。

任意精度演算 (bignum) ライブラリの特性

bignumライブラリは、通常の組み込み型では表現できない非常に大きな数値を扱うためのものです。Go言語の標準ライブラリではmath/bigパッケージがこれに該当します。 これらのライブラリの多くのメソッドは、パフォーマンス最適化のために、演算結果を新しいオブジェクトとして返すのではなく、レシーバ自身を結果として上書きし、そのレシーバを返すという設計になっています。

例: z.Add(x, y)z = x + y を計算し、zを返します。

この設計は、中間オブジェクトの生成を減らし、ガベージコレクションの負荷を軽減する点で効率的ですが、以下のようなメソッドチェーンでは注意が必要です。

// 誤った例(意図しない結果になる可能性)
result.Mul(operandA, result.Sub(operandB, operandC))

この例では、result.Sub(operandB, operandC)が先に実行され、resultoperandB - operandCの結果で上書きされます。その後、result.Mul(operandA, ...)が実行される際には、resultはすでに変更された値になっているため、operandA * (operandB - operandC)ではなく、operandA * (変更されたresult)という意図しない計算が行われる可能性があります。 これを避けるためには、中間結果を一時変数に格納するなどして、レシーバとオペランドのエイリアシング(同じメモリを指すこと)を避ける必要があります。

// 正しい例
temp := new(big.Int).Sub(operandB, operandC)
result.Mul(operandA, temp)

このコミットのbignum関連の修正は、まさにこの種のメソッドチェーンによる計算エラーに対処しています。

技術的詳細

このコミットは、Goコンパイラのwalk.cbignumライブラリのbignum.goおよびbignum_test.goにわたる変更を含んでいます。

src/cmd/gc/walk.c の変更

walk.cはGoコンパイラのバックエンドの一部であり、抽象構文木(AST)を走査し、コード生成のための準備を行う役割を担っています。 変更点:

@@ -1575,6 +1575,7 @@ lookdot(Node *n, Type *t)

 	if(f2 != T) {
 		if(needaddr(n->left->type)) {
+			walktype(n->left, Elv);
 			n->left = nod(OADDR, n->left, N);
 			n->left->type = ptrto(n->left->left->type);
 		}

この変更は、lookdot関数内、特にメソッド呼び出しのレシーバがポインタレシーバを必要とする場合(needaddr(n->left->type)が真の場合)に適用されます。 walktype(n->left, Elv); の追加は、n->left(メソッドレシーバとなる式)が評価される際に、その式がlvalue(アドレス可能)であることを保証するための型チェックと変換処理を強制します。これにより、rvalueがポインタレシーバに誤って「昇格」されるようなケースをコンパイラがより正確に診断できるようになります。もしn->leftがrvalueであれば、walktypeはエラーを報告するか、適切な変換を拒否するはずです。

また、arrayop関数にもif(t == T || tl == T) break;if(t == T) break;といったT(型エラーを示す特別な型)のチェックが追加されています。これは、配列操作の際に型が不正な場合に早期に処理を中断し、コンパイラの堅牢性を高めるためのものです。

src/lib/bignum.go および src/lib/bignum_test.go の変更

これらの変更は、bignumライブラリの計算ロジックとテストの正確性に関するものです。

src/lib/bignum.goRatFromString関数における変更:

@@ -1261,8 +1261,12 @@ export func RatFromString(s string, base uint, slen *int) (*Rational, uint) {
 		talen++;
 		tb, base = NatFromString(s[alen : len(s)], abase, &blen);
 		assert(base == abase);
-		tf := Nat(base).Pow(uint(blen));
-		ta = MakeInt(a.sign, a.mant.Mul(f).Add(b));
+		//BUG f := Nat(base).Pow(uint(blen));
+		tna := Nat(base);
+		tf := na.Pow(uint(blen));
+		//BUG a = MakeInt(a.sign, a.mant.Mul(f).Add(b));
+		tnb := a.mant.Mul(f);
+		ta = MakeInt(a.sign, nb.Add(b));
 		tb = f;
 	}

この変更は、Nat(base).Pow(uint(blen))a.mant.Mul(f).Add(b)というメソッドチェーンを分解し、中間結果を一時変数(na, nb)に格納しています。これは、前述のbignumライブラリのメソッドがレシーバをインプレースで変更する特性に起因するバグを回避するためです。元のコードでは、a.mantMul(f)によって変更された後、その変更されたa.mantに対してAdd(b)が実行される可能性があり、意図しない結果を招いていました。一時変数を使用することで、各演算が独立して行われ、正しい結果が得られるようになります。

src/lib/bignum_test.goの変更:

テストファイルでは、MulTestNatMulTestNatModTestNatLog2TestNatPopといった関数内で同様のパターンが見られます。

例: Mul関数内の変更

@@ -204,11 +204,19 @@ func Mul(x, y bignum.Natural) bignum.Natural {
 	if z1.Cmp(z2) != 0 {
 		tester.Fatalf("multiplication not symmetric:\n\tx = %v\n\ty = %t", x, y);
 	}
-	if !x.IsZero() && z1.Div(x).Cmp(y) != 0 {
-		tester.Fatalf("multiplication/division not inverse (A):\n\tx = %v\n\ty = %t", x, y);
+	// BUG if !x.IsZero() && z1.Div(x).Cmp(y) != 0 {
+	if !x.IsZero()  {
+		tna := z1.Div(x);
+		if na.Cmp(y) != 0 {
+			tester.Fatalf("multiplication/division not inverse (A):\n\tx = %v\n\ty = %t", x, y);
+		}
 	}
-	if !y.IsZero() && z1.Div(y).Cmp(x) != 0 {
-		tester.Fatalf("multiplication/division not inverse (B):\n\tx = %v\n\ty = %t", x, y);
+	// BUG if !y.IsZero() && z1.Div(y).Cmp(x) != 0 {
+	if !y.IsZero() {
+		tnb := z1.Div(y);
+		if nb.Cmp(x) != 0 {
+			tester.Fatalf("multiplication/division not inverse (B):\n\tx = %v\n\ty = %t", x, y);
+		}
 	}
 	return z1;
 }

ここでも、z1.Div(x).Cmp(y)のようなメソッドチェーンがna := z1.Div(x); if na.Cmp(y) != 0のように分解されています。これは、テストの正確性を保証するために、bignum演算の副作用を明示的に管理する必要があることを示しています。テストコード自体が、ライブラリの設計上の特性によって引き起こされる潜在的なバグを回避するように修正されています。

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

src/cmd/gc/walk.c

// lookdot関数内
if(f2 != T) {
    if(needaddr(n->left->type)) {
        walktype(n->left, Elv); // 追加行
        n->left = nod(OADDR, n->left, N);
        n->left->type = ptrto(n->left->left->type);
    }
}

// arrayop関数内
// 複数の箇所で型チェックを追加
if(t == T || tl == T)
    break;
// ...
if(t == T)
    break;
// ...
if(t == T)
    break;

src/lib/bignum.go

// RatFromString関数内
// メソッドチェーンを分解し、一時変数を使用
// 変更前:
// f := Nat(base).Pow(uint(blen));
// a = MakeInt(a.sign, a.mant.Mul(f).Add(b));
// 変更後:
na := Nat(base);
f := na.Pow(uint(blen));
nb := a.mant.Mul(f);
a = MakeInt(a.sign, nb.Add(b));

src/lib/bignum_test.go

// Mul関数内
// メソッドチェーンを分解し、一時変数を使用
// 変更前:
// if !x.IsZero() && z1.Div(x).Cmp(y) != 0 { ... }
// 変更後:
if !x.IsZero()  {
    na := z1.Div(x);
    if na.Cmp(y) != 0 { ... }
}
// 同様に他のテスト関数でも同様の変更が複数箇所に適用

コアとなるコードの解説

src/cmd/gc/walk.c の変更

walktype(n->left, Elv); の追加は、コンパイラがメソッドレシーバの式を評価する際に、それが「アドレス可能なlvalue」であることを明示的に検証するステップを挿入します。これにより、Go言語のポインタレシーバのルール(rvalueには適用されない)がより厳密に強制され、開発者が誤ってrvalueに対してポインタレシーバを持つメソッドを呼び出そうとした場合に、コンパイル時にエラーとして検出できるようになります。これは、実行時エラーや予期せぬ動作を防ぐための重要な診断強化です。 arrayop関数におけるT(型エラー)のチェックは、不正な型が配列操作に渡された場合に、コンパイラがパニックを起こすことなく、より gracefully にエラーを処理するための堅牢性向上です。

src/lib/bignum.go および src/lib/bignum_test.go の変更

これらの変更は、bignumライブラリの設計上の特性(メソッドがレシーバをインプレースで変更する)に起因する計算の不正確さを修正するためのものです。 具体的には、Nat(base).Pow(uint(blen))a.mant.Mul(f).Add(b)のようなメソッドチェーンを、中間結果を明示的な一時変数(na, nbなど)に格納する形に書き換えています。これにより、ある演算の結果が、その後の別の演算のオペランドとして使用される際に、意図せずレシーバが変更されてしまうという「エイリアシング」の問題が回避されます。 テストコードにおいても同様の修正が行われているのは、テストがライブラリの正確性を検証するものであるため、テストコード自体がライブラリの特性によって誤った結果を出すことを防ぐためです。これにより、テストが真にライブラリのバグを検出できるようになります。

これらの変更は、Go言語の初期段階において、言語のセマンティクス(特にポインタとメソッド)とライブラリの設計(特にパフォーマンス最適化のためのインプレース変更)がどのように相互作用し、どのような潜在的な問題を引き起こすか、そしてそれらをどのように解決していくかを示しています。

関連リンク

参考にした情報源リンク