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

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

このコミットは、Go言語の初期のコミットの一つであり、src/lib/bignum.gotest/hilbert.go の2つのファイルを変更しています。bignum.go は多倍長整数(bignum)演算を扱うライブラリの一部であり、hilbert.go はヒルベルト行列に関連するテストコードです。

コミット

commit eb32228627242b043659b2157f5eb157f55bc775
Author: Rob Pike <r@golang.org>
Date:   Sat Dec 20 18:15:34 2008 -0800

    hilbert now runs.
    it's 25% faster and runs with 40% less memory allocation than before
    
    R=rsc
    DELTA=20  (15 added, 0 deleted, 5 changed)
    OCL=21690
    CL=21690

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

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

元コミット内容

hilbert now runs.
it's 25% faster and runs with 40% less memory allocation than before

R=rsc
DELTA=20  (15 added, 0 deleted, 5 changed)
OCL=21690
CL=21690

変更の背景

このコミットの主な目的は、hilbert プログラム(おそらくヒルベルト行列の計算を行うプログラム)のパフォーマンスを向上させることです。コミットメッセージには、「25%高速化され、メモリ割り当てが40%削減された」と明記されており、これは顕著な改善です。この改善は、主に多倍長整数演算(bignum)の効率化と、Go言語の初期段階におけるメモリ管理およびコンパイラの最適化に関する知見に基づいています。

当時のGo言語はまだ開発初期段階であり、ランタイムやコンパイラの最適化は現在ほど成熟していませんでした。そのため、コードの記述方法がパフォーマンスやメモリ使用量に直接的な影響を与えることが多く、特に多倍長整数のような計算負荷の高い処理では、細かな最適化が重要でした。このコミットは、そのような初期の最適化努力の一環として行われたと考えられます。

前提知識の解説

ヒルベルト行列 (Hilbert Matrix)

ヒルベルト行列は、数学の数値解析においてよく登場する特殊な行列です。その要素 $H_{ij}$ は $1 / (i + j - 1)$ で定義されます(1-indexedの場合)。この行列は、非常に条件数が大きく(逆行列を求めるのが難しい)、数値計算において悪条件問題(ill-conditioned problem)の典型例として知られています。そのため、ヒルベルト行列の逆行列計算などは、多倍長精度演算を必要とすることが多く、計算負荷が高い処理となります。test/hilbert.go は、このヒルベルト行列に関連する計算の正確性や性能を検証するためのテストコードであると推測されます。

多倍長整数演算 (Arbitrary-Precision Arithmetic / Bignum)

通常のコンピュータの整数型(例: int32, int64)は、表現できる数値の範囲に限りがあります。しかし、暗号学、数値解析、数論などの分野では、この範囲を超える非常に大きな整数を扱う必要が生じます。多倍長整数演算は、このような任意の精度の整数を扱うための技術です。Go言語では、標準ライブラリの math/big パッケージがこの機能を提供しています。

多倍長整数は通常、内部的に数値の各桁を配列やスライスで保持し、加算、減算、乗算、除算などの演算をソフトウェアで実装します。これらの演算は、ハードウェアが直接サポートする固定長の整数演算に比べて、はるかに多くのCPUサイクルとメモリを消費します。そのため、多倍長整数演算の効率は、それを多用するアプリケーションの全体的なパフォーマンスに大きな影響を与えます。

Go言語のメモリ管理とガベージコレクション (GC)

Go言語はガベージコレクタ(GC)を備えており、開発者が手動でメモリを解放する必要がありません。しかし、GCはプログラムの実行中に不要になったメモリを自動的に回収するプロセスであり、その実行には一定のコストがかかります。特に、短期間に大量のオブジェクトが生成され、すぐに不要になるようなパターン(一時オブジェクトの多発)は、GCの頻度を増やし、プログラムのパフォーマンスを低下させる可能性があります。

Go言語の初期のGCは、現在のバージョンに比べて最適化が進んでいなかったため、メモリ割り当ての削減はパフォーマンス向上に直結する重要な課題でした。オブジェクトの生成を減らすことで、GCの負荷を軽減し、プログラム全体の実行速度を向上させることができます。

技術的詳細

このコミットで行われた技術的な変更は、主に以下の2つのパターンに分類できます。

  1. メソッドチェーンの分解: 複数のメソッド呼び出しを.で連結する「メソッドチェーン」を、中間結果を変数に格納する複数の独立したステートメントに分解しています。
  2. 値渡しへの変更: ポインタ渡しされていた引数を値渡しに変更しています。

これらの変更は、Go言語のコンパイラとランタイムの特性を考慮した、メモリ割り当ての削減と実行速度の向上を目的とした最適化です。

メソッドチェーンの分解

Go言語では、メソッドがレシーバ(メソッドが呼び出されるオブジェクト)をポインタで受け取り、そのレシーバ自身を返す場合、メソッドチェーンが可能になります。しかし、math/bigパッケージのような多倍長整数を扱うライブラリでは、各演算(例: Mul, Add, Div)が新しいbig.Intbig.Naturalオブジェクトを生成して返すことがあります。

例えば、x.Mul(y).Add(z) のようなチェーンでは、x.Mul(y) の結果として一時的なbig.Intオブジェクトが生成され、そのオブジェクトに対してAdd(z)が呼び出されます。この一時オブジェクトは、Addの呼び出しが完了すると不要になり、ガベージコレクションの対象となります。このような一時オブジェクトが大量に生成されると、GCの頻度が増加し、パフォーマンスが低下します。

コミットで行われた変更は、この一時オブジェクトの生成を明示的に制御することを意図しています。中間結果を変数に格納することで、コンパイラがより効率的なコードを生成したり、場合によってはオブジェクトの再利用を促したりする機会が増えます。

値渡しへの変更

test/hilbert.goMakeRat 関数では、引数 x の型が *Big.Natural から Big.Natural に変更されています。

  • *Big.Natural (ポインタ渡し): Big.Natural オブジェクトがヒープに割り当てられ、そのメモリアドレス(ポインタ)が関数に渡されます。関数内でオブジェクトにアクセスする際には、ポインタのデリファレンス(間接参照)が必要になります。
  • Big.Natural (値渡し): Big.Natural オブジェクトのコピーが関数に渡されます。Big.Natural が比較的小さな構造体である場合、コピーのコストはポインタのデリファレンスやヒープ割り当てのコストよりも低くなる可能性があります。また、値渡しにすることで、コンパイラがスタック上にオブジェクトを割り当てることが可能になり、ヒープ割り当てとそれに伴うGCの負荷を軽減できる場合があります。

Go言語の初期のバージョンでは、コンパイラのエスケープ解析(変数がヒープに割り当てられるべきか、スタックに割り当てられるべきかを判断するプロセス)が現在ほど洗練されていなかったため、このような明示的な値渡しへの変更がパフォーマンスに大きな影響を与えることがありました。

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

src/lib/bignum.go

--- a/src/lib/bignum.go
+++ b/src/lib/bignum.go
@@ -793,7 +793,10 @@ export func Fact(n uint) Natural {
 
 
 export func Binomial(n, k uint) Natural {
-	return MulRange(n-k+1, n).Div(MulRange(1, k));
+	//BUG return MulRange(n-k+1, n).Div(MulRange(1, k));
+	x := MulRange(n-k+1, n);
+	y := MulRange(1, k);
+	return x.Div(y);
 }
 
 

test/hilbert.go

--- a/test/hilbert.go
+++ b/test/hilbert.go
@@ -82,7 +82,7 @@ func NewHilbert(n int) *Matrix {
 }
 
 
-func MakeRat(x *Big.Natural) *Big.Rational {
+func MakeRat(x Big.Natural) *Big.Rational {
 	return Big.MakeRat(Big.MakeInt(false, x), Big.Nat(1));
 }
 
@@ -100,7 +100,12 @@ func NewInverseHilbert(n int) *Matrix {
 		x3 := MakeRat(Big.Binomial(uint(n+j), uint(n-i-1)));
 		x4 := MakeRat(Big.Binomial(uint(i+j), uint(i)));
 		x4 = x4.Mul(x4);
-		a.set(i, j, x0.Mul(x1).Mul(x2).Mul(x3).Mul(x4));
+		// BUG a.set(i, j, x0.Mul(x1).Mul(x2).Mul(x3).Mul(x4));
+		y1 := x0.Mul(x1);
+		y2 := y1.Mul(x2);
+		y3 := y2.Mul(x3);
+		y4 := y3.Mul(x4);
+		a.set(i, j, y4);
 		}
 	}
 	return a;
@@ -114,7 +119,11 @@ func (a *Matrix) Mul(b *Matrix) *Matrix {
 		for j := 0; j < c.m; j++ {
 			x := Zero;
 			for k := 0; k < a.m; k++ {
-				x = x.Add(a.at(i, k).Mul(b.at(k, j)));
+				//BUG x = x.Add(a.at(i, k).Mul(b.at(k, j)));
+				a1 := a.at(i, k);
+				b1 := b.at(k, j);
+				a2 := a1.Mul(b1);
+				x = x.Add(a2);
 			}
 			c.set(i, j, x);
 		}
@@ -129,7 +138,10 @@ func (a *Matrix) Eql(b *Matrix) bool {
 	}
 	for i := 0; i < a.n; i++ {
 		for j := 0; j < a.m; j++ {
-			if a.at(i, j).Cmp(b.at(i,j)) != 0 {
+			// BUG if a.at(i, j).Cmp(b.at(i,j)) != 0 {
+			a1 := a.at(i, j);
+			b1 := b.at(i,j);
+			if a1.Cmp(b1) != 0 {
 				return false;
 			}
 		}

コアとなるコードの解説

src/lib/bignum.goBinomial 関数

変更前:

return MulRange(n-k+1, n).Div(MulRange(1, k));

変更後:

x := MulRange(n-k+1, n);
y := MulRange(1, k);
return x.Div(y);

この変更は、MulRange の結果に対して直接 Div を呼び出すメソッドチェーンを、中間変数 xy を介して2つの独立した操作に分解しています。これにより、MulRange が返す一時的な Natural オブジェクトが、Div メソッドに渡される前に明示的な変数にバインドされます。これは、コンパイラが一時オブジェクトの寿命をより正確に管理し、不要になったオブジェクトをより早くGCの対象とすることを可能にする可能性があります。また、Go言語の初期のコンパイラが、このような複雑な式を最適化する際に苦労していた可能性も考えられます。

test/hilbert.goMakeRat 関数

変更前:

func MakeRat(x *Big.Natural) *Big.Rational {

変更後:

func MakeRat(x Big.Natural) *Big.Rational {

引数 x の型が *Big.Natural (ポインタ) から Big.Natural (値) に変更されました。Big.Natural が比較的小さな構造体である場合、ポインタを渡すよりも値渡しの方が効率的である可能性があります。ポインタ渡しは、ヒープ割り当てと間接参照のオーバーヘッドを伴いますが、値渡しはオブジェクトのコピーを伴います。Go言語のコンパイラは、小さな構造体を値渡しする場合、スタック上に割り当てることでヒープ割り当てを回避し、GCの負荷を軽減できる場合があります。この変更は、MakeRat が頻繁に呼び出される場合に、メモリ割り当ての削減に大きく貢献したと考えられます。

test/hilbert.goNewInverseHilbert, Mul, Eql 関数/メソッド

これらの箇所でも、Binomial 関数と同様に、複数のメソッド呼び出しが連結された式が、中間変数を用いた複数の独立したステートメントに分解されています。

例: NewInverseHilbert の変更 変更前:

a.set(i, j, x0.Mul(x1).Mul(x2).Mul(x3).Mul(x4));

変更後:

y1 := x0.Mul(x1);
y2 := y1.Mul(x2);
y3 := y2.Mul(x3);
y4 := y3.Mul(x4);
a.set(i, j, y4);

この変更は、Mul 演算が新しい Big.Rational オブジェクトを返す場合に、連鎖的な呼び出しによって生成される多数の一時オブジェクトを減らすことを目的としています。各中間結果を明示的な変数に格納することで、コンパイラがこれらのオブジェクトの寿命をより正確に追跡し、不要になった時点でより効率的にメモリを再利用できるようになります。これにより、ガベージコレクションの頻度と実行時間が削減され、全体的なパフォーマンスが向上します。

Matrix.MulMatrix.Eql における同様の変更も、同様の理由(一時オブジェクトの削減とGC負荷の軽減)で行われています。特に Matrix.Mul は行列の乗算という計算負荷の高い処理であり、内部で多倍長演算を頻繁に呼び出すため、このような最適化が大きな効果をもたらしたと考えられます。

これらの変更は、Go言語の初期のコンパイラとランタイムの特性を深く理解した上で行われた、非常に実践的なパフォーマンス最適化の例と言えます。

関連リンク

参考にした情報源リンク

  • Go言語のコミット履歴 (GitHub): https://github.com/golang/go
  • 多倍長整数演算に関する一般的な情報 (Wikipediaなど)
  • ヒルベルト行列に関する一般的な情報 (Wikipediaなど)
  • Go言語の初期のパフォーマンス最適化に関する議論(当時のメーリングリストやフォーラムなど、一般にはアクセスしにくい情報源も含まれる可能性があります)I have generated the explanation based on the provided structure and the commit details. I have included detailed explanations for each section, leveraging my knowledge of Go, numerical computing, and software engineering principles. I have also tried to infer the context of the changes given the age of the commit and the early stage of Go development.

I will now output the generated explanation to standard output.