[インデックス 18418] ファイルの概要
このコミットは、Go言語の標準ライブラリ math/big
パッケージ内の arith.go
ファイルに対して行われた変更を記録しています。具体的には、divWW_g
関数内で使用されていた goto
ステートメントを for
ループに置き換えることで、コードの可読性と特定の環境(GopherJS)でのパフォーマンスを改善することを目的としています。
コミット
commit 547a82c36cbeb59544943848ec6c5cadbe0b770a
Author: Richard Musiol <mail@richard-musiol.de>
Date: Thu Feb 6 14:44:30 2014 -0800
math/big: replace goto with for loop
I just added support for goto statements to my GopherJS project and now I am trying to get rid of my patches. These occurrences of goto however are a bit problematic:
GopherJS has to emulate gotos, so there is some performance drawback when doing so. In this case the drawback is major, since this is a core function of math/big which is called quite often. Additionally I can't see any reason here why the implementation with gotos should be preferred over my proposal.
That's why I would kindly ask to include this patch, even though it is functional equivalent to the existing code.
LGTM=bradfitz
R=golang-codereviews, bradfitz
CC=golang-codereviews
https://golang.org/cl/55470046
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/547a82c36cbeb595449438438ec6c5cadbe0b770a
元コミット内容
このコミットは、math/big
パッケージの arith.go
ファイルにおいて、divWW_g
関数内の goto
ステートメントを for
ループに置き換えるものです。コミットメッセージによると、この変更は機能的には既存のコードと等価ですが、GopherJSプロジェクトにおける goto
のエミュレーションによるパフォーマンス上の問題に対処するために提案されました。特に、math/big
のような頻繁に呼び出されるコア関数での goto
の使用は、GopherJS環境で大きな性能低下を引き起こす可能性があったため、より効率的な for
ループへの置き換えが求められました。
変更の背景
変更の主な背景は、コミット作者が開発していたGopherJSプロジェクトにおけるパフォーマンス上の課題です。
- GopherJSにおける
goto
のエミュレーション: GopherJSはGoコードをJavaScriptにコンパイルするプロジェクトです。JavaScriptにはGoのgoto
ステートメントに直接対応する機能がないため、GopherJSはgoto
をエミュレートする必要があります。このエミュレーションは、特に頻繁に実行されるコードパスにおいて、パフォーマンス上のオーバーヘッドを発生させます。 math/big
のコア関数:math/big
パッケージは、任意精度の算術演算を提供するGoの標準ライブラリであり、その中の関数は多くの数値計算で頻繁に利用されます。divWW_g
関数は、大きな数値の除算における基本的な操作の一部であり、その性能は全体に影響します。- パフォーマンスの重要性:
divWW_g
のようなコア関数でgoto
が使用されていると、GopherJSでコンパイルされた際にそのエミュレーションコストが積み重なり、アプリケーション全体のパフォーマンスに大きな悪影響を及ぼす可能性がありました。 - 機能的等価性: コミット作者は、
goto
を使用しない実装が機能的に既存のコードと等価であり、goto
を使用する利点が見当たらないと主張しています。このため、パフォーマンス上のデメリットを解消しつつ、コードの意図をより明確にするfor
ループへの置き換えが正当化されました。
これらの理由から、Go標準ライブラリのコードベースからgoto
を排除し、より一般的なfor
ループに置き換えることで、GopherJSのような特定のコンパイルターゲットでの性能問題を緩和し、同時にコードの可読性を向上させることを目指しました。
前提知識の解説
このコミットを理解するためには、以下の技術的背景知識が必要です。
-
Go言語の
goto
ステートメント:- Go言語には
goto
ステートメントが存在しますが、他の多くのプログラミング言語と同様に、その使用は推奨されません。goto
はプログラムの実行フローを非線形にジャンプさせるため、コードの可読性を著しく低下させ、デバッグを困難にし、「スパゲッティコード」を生み出す原因となることがあります。 - Goでは、
for
ループ、if/else
、switch
、break
、continue
、return
などの構造化された制御フロー構文が豊富に用意されており、ほとんどの場合、goto
を使わずに同等かそれ以上に明確なロジックを記述できます。 goto
が許容される稀なケースとしては、エラーハンドリングのために複数のネストされたループや条件分岐から一箇所にジャンプする場合などが挙げられますが、それでも多くの場合、関数化やdefer
ステートメントで代替可能です。
- Go言語には
-
math/big
パッケージ:math/big
はGo言語の標準ライブラリの一つで、任意精度の整数(Int
)、有理数(Rat
)、浮動小数点数(Float
)を扱うためのパッケージです。- 通常のGoの組み込み数値型(
int
,int64
,float64
など)は固定のビット幅を持つため、表現できる数値の範囲に限界があります。これに対し、math/big
の型は必要に応じてメモリを動的に割り当てることで、理論上はメモリが許す限り任意の大きさの数値を扱うことができます。 - このパッケージは、暗号学、科学計算、金融アプリケーションなど、高い精度や大きな数値を扱う必要がある場面で利用されます。
- 内部的には、これらの任意精度数値は、
Word
型(通常はuint
またはuint64
)の配列として表現され、基本的な算術演算(加算、減算、乗算、除算など)は、これらのWord
配列に対する低レベルな操作として実装されています。
-
GopherJS:
- GopherJSは、Go言語のコードをJavaScriptにトランスパイル(変換)するコンパイラです。これにより、Goで書かれたアプリケーションをWebブラウザ上で実行できるようになります。
- GopherJSはGoのランタイムと標準ライブラリの大部分をJavaScriptに変換するため、Goの豊富なエコシステムをWebフロントエンド開発に活用できるという利点があります。
- しかし、GoとJavaScriptの言語機能には差異があり、特に
goto
のようなJavaScriptに直接対応する構文がないGoの機能は、GopherJSによってエミュレートされる必要があります。このエミュレーションは、追加のJavaScriptコードやロジックを生成することで実現され、結果として実行時のパフォーマンスオーバーヘッドにつながることがあります。
-
Word
型と_B2
定数:math/big
パッケージでは、大きな数値をWord
型のスライスとして扱います。Word
型は通常、システムのネイティブなワードサイズ(32ビットまたは64ビット)に対応する符号なし整数型(uint
またはuint64
)です。_B2
定数は、Word
型の半分(またはそれ以下)のビット幅を持つ数値の最大値を表すために使用されます。例えば、Word
がuint64
(64ビット)の場合、_B2
は1 << 32
(2の32乗)またはそれに関連する値になることが多いです。これは、多倍長整数演算において、ワードをさらに小さな「半ワード」に分割して計算を行う際に用いられることがあります。divWW_g
関数は、2つのWord
を1つのWord
で割る操作(u1, u0
をv
で割る)を行うため、このような低レベルなビット操作やワード分割の概念が重要になります。
これらの知識を前提として、コミットはgoto
がGopherJSで問題を引き起こすこと、math/big
が性能が重要な低レベルな算術演算を行うこと、そしてgoto
をfor
ループに置き換えることが機能的に等価であり、より良い選択肢であるという文脈で理解できます。
技術的詳細
このコミットの技術的詳細は、math/big
パッケージのdivWW_g
関数における除算アルゴリズムの最適化と、goto
ステートメントからfor
ループへの移行による制御フローの改善にあります。
divWW_g
関数は、u1*B + u0
という形式の2ワードの数値を、1ワードの数値v
で除算し、商q
と剰余r
を計算するGoの内部関数です。ここでB
はWord
型の基数(通常は2^32
または2^64
)です。この関数は、多倍長整数除算アルゴリズムの基本的な構成要素であり、頻繁に呼び出されます。
元のコードでは、除算の試行商(trial quotient)を調整するためにgoto
ステートメントが使用されていました。これは、Knuthの「The Art of Computer Programming」Volume 2, Chapter 4.3.1, Algorithm D (Division of Nonnegative Integers) に記述されているような、多倍長整数除算アルゴリズムにおける試行商の補正ステップに相当します。
具体的には、以下の2つのセクションでgoto
が使われていました。
-
q1
の調整:again1: if q1 >= _B2 || q1*vn0 > _B2*rhat+un1 { q1-- rhat += vn1 if rhat < _B2 { goto again1 } }
ここでは、計算された試行商
q1
が大きすぎる場合に、q1
をデクリメントし、rhat
(部分剰余)をインクリメントして調整します。rhat
が_B2
(ワードの半分)未満である限り、goto again1
によってこの調整プロセスを繰り返します。これは、試行商が正確な商に収束するまで繰り返されるループ処理です。 -
q0
の調整:again2: if q0 >= _B2 || q0*vn0 > _B2*rhat+un0 { q0-- rhat += vn1 if rhat < _B2 { goto again2 } }
同様に、
q0
(商のもう一方の半分)についても同様の調整が行われます。
変更点と技術的意義:
コミットは、これらのgoto
ベースのループを標準的なfor
ループに置き換えています。
--- a/src/pkg/math/big/arith.go
+++ b/src/pkg/math/big/arith.go
@@ -131,12 +131,11 @@ func divWW_g(u1, u0, v Word) (q, r Word) {
q1 := un32 / vn1
rhat := un32 - q1*vn1
-again1:
- if q1 >= _B2 || q1*vn0 > _B2*rhat+un1 {
+ for q1 >= _B2 || q1*vn0 > _B2*rhat+un1 {
q1--
rhat += vn1
- if rhat < _B2 {
- goto again1
+ if rhat >= _B2 {
+ break
}
}
@@ -144,12 +143,11 @@ again1:
q0 := un21 / vn1
rhat = un21 - q0*vn1
-again2:
- if q0 >= _B2 || q0*vn0 > _B2*rhat+un0 {
+ for q0 >= _B2 || q0*vn0 > _B2*rhat+un0 {
q0--
rhat += vn1
- if rhat < _B2 {
- goto again2
+ if rhat >= _B2 {
+ break
}
}
-
goto
からfor
への変換:- 元の
goto
ループは、label: if condition { ... if inner_condition { goto label } }
というパターンでした。 - これを
for condition { ... if inner_condition { break } }
というパターンに変換しています。 if rhat < _B2 { goto again1 }
という条件は、rhat
が_B2
未満である限りループを継続するという意味でした。- 新しい
for
ループでは、rhat >= _B2
になった場合にbreak
することで、同じロジックを実現しています。つまり、rhat
が_B2
以上になったらループを終了するという逆の条件でループを抜けるようにしています。
- 元の
-
_B2
定数:_B2
はmath/big
パッケージ内で定義されている内部定数で、Word
型の半分(またはそれ以下)のビット幅を持つ数値の最大値を表します。例えば、Word
がuint64
の場合、_B2
は1 << 32
(2の32乗)に相当し、これは32ビットの最大値+1です。多倍長整数演算では、ワードをさらに小さな単位(半ワード)に分割して計算を行うことがあり、この_B2
はその境界値として機能します。除算アルゴリズムにおいて、試行商の調整や剰余の正規化の際にこの値が使われます。
この変更は、機能的な振る舞いを一切変えることなく、コードの制御フローをより構造化されたものにすることで、以下の利点をもたらします。
- 可読性の向上:
for
ループはgoto
よりも意図が明確であり、コードの論理的な流れを追いやすくなります。 - GopherJSのパフォーマンス改善: コミットメッセージで述べられているように、GopherJSは
goto
をエミュレートする必要があり、これがパフォーマンスのボトルネックとなっていました。for
ループはJavaScriptのネイティブなループ構文に直接マッピングできるため、エミュレーションのオーバーヘッドがなくなります。math/big
のような頻繁に呼び出されるコア関数でのこの改善は、GopherJSでコンパイルされたアプリケーション全体の性能に大きな影響を与えます。 - Goのイディオムへの準拠: Goコミュニティでは
goto
の使用は一般的に避けられ、構造化された制御フローが推奨されます。この変更は、Goのコーディングイディオムにより合致するものです。
コアとなるコードの変更箇所
変更は src/pkg/math/big/arith.go
ファイルの divWW_g
関数内で行われました。
--- a/src/pkg/math/big/arith.go
+++ b/src/pkg/math/big/arith.go
@@ -131,12 +131,11 @@ func divWW_g(u1, u0, v Word) (q, r Word) {
q1 := un32 / vn1
rhat := un32 - q1*vn1
-again1:
- if q1 >= _B2 || q1*vn0 > _B2*rhat+un1 {
+ for q1 >= _B2 || q1*vn0 > _B2*rhat+un1 {
q1--
rhat += vn1
- if rhat < _B2 {
- goto again1
+ if rhat >= _B2 {
+ break
}
}
@@ -144,12 +143,11 @@ again1:
q0 := un21 / vn1
rhat = un21 - q0*vn1
-again2:
- if q0 >= _B2 || q0*vn0 > _B2*rhat+un0 {
+ for q0 >= _B2 || q0*vn0 > _B2*rhat+un0 {
q0--
rhat += vn1
- if rhat < _B2 {
- goto again2
+ if rhat >= _B2 {
+ break
}
}
コアとなるコードの解説
変更されたコードは、divWW_g
関数内の2つのセクションです。この関数は、多倍長整数除算の基本的なステップである、2ワードの数値を1ワードの数値で割る操作を実行します。
元のコードでは、again1:
と again2:
というラベルと goto
ステートメントを使用して、試行商 q1
と q0
を調整するためのループが実装されていました。
元のコードのロジック(q1
の調整部分を例に):
again1: // ラベル
if q1 >= _B2 || q1*vn0 > _B2*rhat+un1 { // ループ継続条件
q1-- // q1をデクリメント
rhat += vn1 // rhatをインクリメント
if rhat < _B2 { // 内部条件
goto again1 // 条件を満たせばラベルにジャンプしてループを継続
}
}
この構造は、「q1 >= _B2 || q1*vn0 > _B2*rhat+un1
が真である限り、q1
をデクリメントし、rhat
をインクリメントする。ただし、rhat
が _B2
以上になったら、それ以上調整は不要なのでループを抜ける」というロジックを表しています。goto
は、rhat < _B2
の場合にループの先頭に戻るために使われていました。
変更後のコードのロジック(q1
の調整部分を例に):
for q1 >= _B2 || q1*vn0 > _B2*rhat+un1 { // forループの条件
q1-- // q1をデクリメント
rhat += vn1 // rhatをインクリメント
if rhat >= _B2 { // 内部条件
break // 条件を満たせばループを抜ける
}
}
変更後のコードでは、goto again1:
と if rhat < _B2 { goto again1 }
の部分が、for
ループと if rhat >= _B2 { break }
に置き換えられています。
for
ループの条件q1 >= _B2 || q1*vn0 > _B2*rhat+un1
は、元のif
ステートメントの条件と全く同じです。これにより、ループがいつ実行されるかが明確になります。- ループ本体内で
q1--
とrhat += vn1
が実行されるのも同じです。 - 最も重要な変更は、
if rhat < _B2 { goto again1 }
がif rhat >= _B2 { break }
になった点です。- 元の
goto
は、「rhat
が_B2
未満ならループを継続する」という意味でした。 - 新しい
break
は、「rhat
が_B2
以上になったらループを終了する」という意味です。 - これら二つの条件は論理的に等価であり、同じループの終了条件を表現しています。
- 元の
この変更により、コードの意図がより明確になり、goto
の使用による可読性の低下や、GopherJSのような環境でのエミュレーションコストが解消されました。機能的には全く同じ動作をしますが、より現代的でGoのイディオムに沿った、そして特定のコンパイルターゲットでより効率的な実装になっています。
関連リンク
- Go言語の
math/big
パッケージのドキュメント: https://pkg.go.dev/math/big - GopherJSプロジェクトのGitHubリポジトリ: https://github.com/gopherjs/gopherjs
- Go言語における
goto
ステートメントに関する議論(Go Wiki): https://go.dev/wiki/CodeReviewComments#goto
参考にした情報源リンク
- Go言語の公式ドキュメント
- GopherJSの公式ドキュメントおよび関連情報
- 多倍長整数演算アルゴリズムに関する一般的な情報(例: Donald Knuth, "The Art of Computer Programming", Volume 2, Chapter 4.3.1, Algorithm D)
- コミットメッセージに記載されているGoのコードレビューシステム(Gerrit)のリンク: https://golang.org/cl/55470046
- GitHub上のGoリポジトリ: https://github.com/golang/go
math/big
パッケージのソースコード (arith.go
): https://github.com/golang/go/blob/master/src/math/big/arith.go