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

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

このコミットは、Go言語の標準ライブラリの一部であるsrc/lib/bignum.go(多倍長整数演算ライブラリ)と、そのテストファイルsrc/lib/bignum_test.go、そしてビルド設定ファイルsrc/lib/Makefileに対する変更を含んでいます。主な変更点は、bignumパッケージ内の関数シグネチャと変数宣言におけるポインタ型(*Natural)から値型(Natural)への移行です。これは、当時のGoコンパイラ6gのバグを回避し、新しい言語の「レジーム」(規則や慣習)に適合させるための対応として記述されています。

コミット

commit 9ccf39bd684d5d6384259cedd9ee0f567796225b
Author: Rob Pike <r@golang.org>
Date:   Sat Dec 20 17:25:43 2008 -0800

    update to new regime.
    lines marked BUG are rewrites working around 6g bug.
    
    R=rsc
    DELTA=161  (42 added, 2 deleted, 117 changed)
    OCL=21689
    CL=21689

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

https://github.com/golang/go/commit/9ccf39bd684d5d6384259cedd9ee0f567796225b

元コミット内容

update to new regime.
lines marked BUG are rewrites working around 6g bug.

R=rsc
DELTA=161  (42 added, 2 deleted, 117 changed)
OCL=21689
CL=21689

変更の背景

このコミットの背景には、Go言語の初期開発段階におけるコンパイラの挙動と、言語設計の進化があります。コミットメッセージにある「new regime」(新しい体制/規則)と「6g bug」という記述がその核心を示しています。

当時のGo言語はまだ開発途上にあり、コンパイラ(特に6g、x86-64アーキテクチャ向けのGoコンパイラ)には、特定のコードパターン、特にポインタの扱いに関してバグが存在していたと考えられます。bignumパッケージのような数値演算ライブラリでは、パフォーマンスとメモリ効率のためにポインタを多用することが一般的ですが、このバグが原因で予期せぬ動作やコンパイルエラーが発生していた可能性があります。

また、「new regime」という言葉は、Go言語の型システムやメモリ管理、あるいは関数呼び出し規約に関する内部的な変更を示唆しています。Go言語は、値渡しとポインタ渡しのセマンティクスを明確に区別しますが、初期の段階ではその実装や推奨されるパターンが固まっていなかった可能性があります。このコミットは、コンパイラのバグを回避しつつ、Go言語が目指すセマンティクス(おそらく値渡しをより推奨する方向性)にコードベースを適合させるための対応であったと推測されます。

具体的には、多倍長整数のような大きなデータ構造を扱う場合、通常はコピーコストを避けるためにポインタで渡すのが効率的です。しかし、コンパイラのバグや新しい言語設計の方向性により、一時的に値渡しに切り替えるか、あるいは値渡しでも効率的に動作するようにコードを調整する必要があったと考えられます。

前提知識の解説

Go言語の初期開発とコンパイラ (6g)

Go言語は2007年末に設計が始まり、2009年にオープンソースとして公開されました。このコミットが行われた2008年12月は、Go言語がまだ活発に開発されていた初期段階にあたります。

  • 6g: Go言語の初期のコンパイラ群は、ターゲットアーキテクチャに基づいて命名されていました。6gはx86-64(64ビットIntel/AMD)アーキテクチャ向けのGoコンパイラを指します。他にも8g(x86-32)、5g(ARM)などがありました。これらのコンパイラは、現在のgo buildコマンドが内部的に使用するcmd/compileとは異なり、より低レベルなツールとして機能していました。初期のGoコンパイラは、C言語で書かれたgc(Go Compiler)というコンパイラ群の一部でした。
  • コンパイラのバグ: ソフトウェア開発の初期段階では、コンパイラ自体にバグが含まれていることは珍しくありません。特に、新しい言語機能や最適化が導入される際には、予期せぬ挙動を引き起こすことがあります。このコミットで言及されている「6g bug」は、bignumパッケージの特定のコードパターンが6gコンパイラで正しく処理されない問題であったと推測されます。

Go言語におけるポインタと値のセマンティクス

Go言語は、C++やJavaのようなオブジェクト指向言語とは異なるアプローチで型とメモリを扱います。

  • 値型と参照型: Goには厳密な意味での「参照型」という概念はありませんが、ポインタ(*T)を使用することで、変数のアドレスを渡し、間接的にその値を操作することができます。スライス、マップ、チャネルなどは内部的にポインタのような振る舞いをしますが、これらは「参照型」ではなく「ヘッダにポインタを含む値型」と理解するのが適切です。
  • 値渡し: Goの関数引数は常に値渡しです。これは、引数として渡された変数のコピーが関数内で使用されることを意味します。構造体のような複合型を値渡しすると、その構造体全体のコピーが作成されます。
  • ポインタ渡し: 変数のアドレス(ポインタ)を値として渡すことで、関数内で元の変数を変更できます。*T型はポインタ型であり、そのポインタが指す先の値は*ptrでアクセスできます。
  • newキーワード: Goのnew組み込み関数は、指定された型のゼロ値に初期化された新しい項目を割り当て、そのアドレス(ポインタ)を返します。例えば、new(Natural)*Natural型のポインタを返します。
  • &演算子: 変数のアドレスを取得するために使用されます。例えば、&myVarmyVarのアドレスを返します。

このコミットでは、*NaturalからNaturalへの変更が多用されており、これはポインタ渡しから値渡しへの移行、またはポインタのデリファレンス(*)とアドレス取得(&)の明示的な使用を伴う変更を示しています。

多倍長整数 (bignum)

bignumパッケージは、Go言語で任意の精度の整数(非常に大きな整数)を扱うためのライブラリです。通常のintint64型では表現できない桁数の数値を扱う際に使用されます。

  • Digit: bignumパッケージでは、多倍長整数を構成する「桁」を表すためにDigit型が定義されています。これは通常、uintuint32などの符号なし整数型に対応します。
  • Natural: []Digit(Digitのスライス)として定義されており、符号なしの多倍長整数を表します。スライスはGoにおいて参照セマンティクスを持つ値型であるため、スライス自体を値渡ししても、その内部の配列は共有されます。しかし、スライスヘッダ(ポインタ、長さ、容量)はコピーされます。

技術的詳細

このコミットの技術的な詳細は、主にbignum.goにおけるNatural型の扱い方の変更に集約されます。

  1. *NaturalからNaturalへの変更:

    • 多くの関数シグネチャで、引数や戻り値の型が*bignum.Naturalからbignum.Naturalに変更されています。
    • 例: func (x *Natural) Add(y *Natural) *Naturalfunc (x *Natural) Add(y Natural) Natural に変更。
    • これは、多倍長整数オブジェクト自体をポインタで渡すのではなく、値として渡すことを意味します。Goのスライスはヘッダが値渡しされるため、スライス自体を値渡ししても、その基盤となる配列は共有されます。しかし、関数内でスライスを再割り当て(newなど)すると、新しい基盤配列が作成され、元のスライスとは異なるものになります。
    • この変更は、6gコンパイラのポインタ関連のバグを回避するため、またはGo言語の新しい推奨されるコーディングスタイルに合わせるためのものです。
  2. new(*Natural, ...)からnew(Natural, ...)への変更:

    • new組み込み関数の使用方法が変更されています。
    • 例: z := new(*Natural, n + 1)z := new(Natural, n + 1) に変更。
    • new(*Natural, ...)*Natural型のポインタを生成し、そのポインタが指す先はNatural型です。これは二重ポインタのような状況を生み出す可能性があります。
    • new(Natural, ...)Natural型のポインタを生成します。
    • この変更は、Natural型のインスタンスを直接指すポインタを生成するように修正し、ポインタのポインタのような複雑さを避けることを目的としていると考えられます。
  3. ポインタの明示的なデリファレンスとアドレス取得:

    • *x(デリファレンス)や&t(アドレス取得)がコードの随所に導入されています。
    • 例: return y.Add(x)return y.Add(*x) に変更。
    • 例: t, d = DivMod1(t, Digit(base))t, d = DivMod1(&t, Digit(base)) に変更。
    • これは、関数が値を受け取るようになったため、元のポインタが指す値にアクセスするために明示的なデリファレンスが必要になったり、あるいは関数がポインタを期待する場合に値のアドレスを渡す必要が生じたためです。
    • 特に、DivMod1MulAdd1のように、引数として受け取ったNatural型の値を関数内で変更する(in-place operation)場合、値渡しではその変更が呼び出し元に反映されないため、ポインタを渡す必要があります。このため、&tのようにアドレスを渡すように修正されています。
  4. テストコードの修正:

    • bignum_test.goでも、bignum.Naturalを引数として受け取るテストヘルパー関数(NAT_EQ, Add, Sum, Mulなど)のシグネチャが*bignum.Naturalからbignum.Naturalに変更されています。
    • テスト内のNAT_EQ関数では、エラーメッセージのフォーマット文字列がx = %v\ny = %vからx = %v\ny = %vに変更され、引数もx, yから&x, &yに変更されています。これは、%vフォーマット指定子がポインタを受け取った場合にその値を出力するように、あるいは値型になったxyのアドレスを渡すことで、より詳細なデバッグ情報を提供するためと考えられます。
    • テストケースのインデックスが変更されたり、//BUGコメントが追加されたりしています。これは、コンパイラのバグを回避するためのコード変更がテストの挙動に影響を与え、一時的にテストを無効化したり、新しいテストパターンを導入したりする必要があったことを示唆しています。

これらの変更は、Go言語の型システムとコンパイラの成熟に伴う、より堅牢で予測可能なコードの記述方法への移行を示しています。

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

src/lib/bignum.go

  • 定数初期化: NatZero, NatOne, NatTwo, NatTenの初期化から*&が削除され、直接Natural{}Natural{1}のように値で初期化されるようになりました。
    -	NatZero Natural = *&Natural{};
    -	NatOne Natural = *&Natural{1};
    -	NatTwo Natural = *&Natural{2};
    -	NatTen Natural = *&Natural{10};
    +	NatZero Natural = Natural{};
    +	NatOne Natural = Natural{1};
    +	NatTwo Natural = Natural{2};
    +	NatTen Natural = Natural{10};
    
  • 関数シグネチャの変更: Normalize, Add, Sub, Mul, Unpack, Pack, Div, Mod, DivMod, Shl, Shr, And, Or, Xor, Cmp, DivMod1, MulAdd1, NatFromString, Pow, MulRange, Fact, Binomial, Gcd, MakeInt, MulNat, MakeRatなど、多くのメソッドや関数の引数や戻り値の型が*NaturalからNaturalに変更されています。
    -func Normalize(x *Natural) Natural {
    +func Normalize(x Natural) Natural {
    
    -func (x *Natural) Add(y *Natural) *Natural {
    +func (x *Natural) Add(y Natural) Natural {
    
  • newの引数変更: new(*Natural, ...)new(Natural, ...)に変更されています。
    -	z := new(*Natural, n + 1);
    +	z := new(Natural, n + 1);
    
  • ポインタのデリファレンスとアドレス取得:
    • y.Add(x)y.Add(*x)に。
    • Unpack(x)Unpack(*x)に。
    • Shl(z[m-n : m], x, s%W)Shl(z[m-n : m], *x, s%W)に。
    • Shr(z, x[n-m : n], s%W)Shr(z, (*x)[n-m : n], s%W)に。
    • Copy(z[m : n], x[m : n])Copy(z[m : n], (*x)[m : n])に。
    • DivMod1(t, Digit(base))DivMod1(&t, Digit(base))に。
    • MulAdd1(x, Digit(base), Digit(d))MulAdd1(&x, Digit(base), Digit(d))に。
    • Powメソッド内でx := *xpのように、レシーバのポインタをデリファレンスして値として扱うように変更。
    • Gcdメソッド内でx := *xpのように、レシーバのポインタをデリファレンスして値として扱うように変更。
  • //BUGコメントの追加: コンパイラのバグを回避するためのコード変更箇所に//BUGコメントが追加されています。これは、将来的にコンパイラのバグが修正された際に、より簡潔な元のコードに戻すためのマーカーとして機能します。

src/lib/bignum_test.go

  • テストヘルパー関数のシグネチャ変更: NatFromString, NAT_EQ, Add, Sum, Mulなどの関数が、*bignum.Naturalを引数に取る代わりにbignum.Naturalを引数に取るように変更されています。
  • NAT_EQのフォーマット文字列と引数変更: tester.Fatalfのフォーマット文字列と引数が、値型になったxyのアドレスを渡すように修正されています。
    -	tester.Fatalf("TEST failed: %s (%d)\nx = %v\ny = %v", test_msg, n, x, y);
    +	tester.Fatalf("TEST failed: %s (%d)\nx = %v\ny = %v", test_msg, n, &x, &y);
    
  • テストケースの修正と//BUGコメント: TestNatConv, TestRatConv, TestNatAdd, TestNatDiv, TestNatShift, TestNatGcd, TestNatPopなどのテスト関数で、//BUGコメントが追加され、コンパイラのバグを回避するためのコードがテスト内でも明示的に記述されています。これは、テストが元の意図通りに動作するように、一時的なワークアラウンドを適用していることを示します。

src/lib/Makefile

  • bignumのコメントアウトが解除され、ビルド対象に含まれるようになりました。
    -
    -#	bignum\
    +	bignum\
    

コアとなるコードの解説

このコミットのコアとなる変更は、Go言語のbignumパッケージにおける多倍長整数Natural型の扱い方を、ポインタベースから値ベースへとシフトさせた点にあります。

Go言語では、関数に引数を渡す際、常に値渡しが行われます。これは、引数のコピーが作成され、関数内でそのコピーが操作されることを意味します。もし関数内で元の変数を変更したい場合は、その変数のアドレス(ポインタ)を渡す必要があります。

変更前は、bignum.Natural型のオブジェクトをポインタ(*Natural)として関数に渡していました。これは、大きなデータ構造である多倍長整数をコピーするコストを避けるための一般的な最適化手法です。しかし、コミットメッセージにあるように、当時の6gコンパイラにはポインタの扱いにバグがあったため、このポインタベースの設計が問題を引き起こしていました。

変更後、多くの関数シグネチャで引数や戻り値の型が*NaturalからNaturalに変更されました。これは、Natural型([]Digit、つまりDigitのスライス)を値として渡すことを意味します。Goのスライスは、内部的にはポインタ、長さ、容量の3つの要素を持つ構造体です。スライスを値渡しすると、この3つの要素がコピーされますが、スライスが指す基盤となる配列自体はコピーされません。したがって、関数内でスライスの要素を変更した場合、その変更は元のスライスにも反映されます。

しかし、関数内でスライスをnewで再割り当てしたり、新しいスライスを作成して代入したりすると、それは新しい基盤配列を指すことになり、元のスライスとは独立したものになります。このため、DivMod1MulAdd1のように、関数内で引数として受け取ったNatural型の値を「インプレース」(その場で)変更する操作を行う場合、値渡しではその変更が呼び出し元に反映されません。この問題を解決するため、これらの関数では引数に&tのように明示的にアドレスを渡すように修正されています。

また、PowGcdメソッドでは、レシーバがポインタ型(*Natural)であるにもかかわらず、メソッド内でx := *xpのようにデリファレンスして値型としてxを宣言しています。これは、メソッド内でxを操作する際に、xが値型であることによってコンパイラのバグを回避しつつ、必要に応じてxのコピーを作成して操作することで、元のレシーバの状態を保護する意図があると考えられます。

//BUGコメントは、これらの変更が一時的なワークアラウンドであり、コンパイラのバグが修正されれば、よりシンプルで効率的な元のポインタベースのコードに戻すことができるという開発者の意図を示しています。これは、Go言語の初期開発における試行錯誤と、コンパイラの成熟を待つ期間のコード管理戦略を反映しています。

総じて、このコミットは、Go言語の初期段階におけるコンパイラの制約と、それに対応するためのコードベースの適応、そして言語設計の進化の過程を垣間見ることができる貴重な例と言えます。

関連リンク

参考にした情報源リンク

これらの情報源を総合的に参照し、コミットの背景、技術的詳細、およびGo言語の進化におけるその位置づけを分析しました。