[インデックス 1205] ファイルの概要
このコミットは、Go言語の標準ライブラリ math
パッケージ内の Pow
(累乗) 関数の実装を、よりシンプルで理解しやすい形に改善するものです。具体的には、浮動小数点数の内部表現(仮数部と指数部)をより明確に扱うように変数をリファクタリングし、計算ロジックを整理しています。
コミット
commit 2e7e76073adeafa07b444ea673507a974659831f
Author: Russ Cox <rsc@golang.org>
Date: Thu Nov 20 11:56:48 2008 -0800
slightly simpler math.Pow per gri's suggestion
R=gri
DELTA=28 (2 added, 9 deleted, 17 changed)
OCL=19707
CL=19707
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/2e7e76073adeafa07b444ea673507a974659831f
元コミット内容
このコミットは、src/lib/math/pow.go
ファイルに対して行われました。主な変更は、Pow
関数の内部で累乗計算の中間結果を保持する変数の命名と、それらの変数を操作するロジックの整理です。
変更前は、ans
と exp
という変数が使われていましたが、ans
が最終的な結果の仮数部と指数部が混在したような状態になりがちでした。変更後は、a1
を仮数部、ae
を指数部として明確に分離し、a1 * 2^ae
という形式で中間結果を管理するようにしました。これにより、コードの意図がより明確になり、計算ロジックがシンプルになりました。
変更の背景
コミットメッセージにある "slightly simpler math.Pow per gri's suggestion" (griの提案により、math.Pow
を少しシンプルに) という記述から、この変更はRobert Griesemer (Go言語の共同設計者の一人であり、初期のGoプロジェクトにおける重要な貢献者) からの提案に基づいていることがわかります。
math.Pow
のような基本的な数学関数は、正確性、効率性、そしてコードの可読性が非常に重要です。初期のGo言語開発段階では、これらの関数の実装は試行錯誤の段階にあり、より良いアルゴリズムや実装パターンが常に模索されていました。このコミットは、既存の実装をより洗練された形に改善し、将来的なメンテナンスや理解を容易にすることを目的としています。特に、浮動小数点数の特性を考慮した上で、計算のステップをより論理的に整理することが狙いだったと考えられます。
前提知識の解説
浮動小数点数 (Floating-Point Numbers)
コンピュータにおける浮動小数点数は、実数を近似的に表現するための形式です。IEEE 754標準が広く用いられており、数値は通常、符号部 (sign) * 仮数部 (mantissa) * 基数 (radix)^指数部 (exponent)
の形式で表現されます。Go言語の float64
はこの標準に準拠しています。
- 仮数部 (Mantissa / Significand): 数値の有効数字部分。
- 指数部 (Exponent): 基数(通常は2)を何乗するかを示す部分。これにより、非常に大きな数から非常に小さな数までを表現できます。
math.Pow(x, y)
関数
math.Pow(x, y)
は x
を y
乗する関数です。数学的には x^y
と書かれます。一般的に、この計算は e^(y * ln(x))
の関係を利用して行われます。しかし、浮動小数点数の精度や特殊なケース(例: x=0
, y=0
, x=NaN
, y=Inf
など)を考慮すると、単純な対数・指数関数だけでは不十分であり、より複雑なロジックが必要になります。
sys.frexp
と sys.ldexp
Go言語の math
パッケージ(初期のGoでは sys
パッケージの一部として提供されていた可能性もありますが、現在の math
パッケージに相当)には、浮動小数点数を操作するための低レベルな関数が含まれています。
frexp(x float64) (frac float64, exp int)
: この関数は、浮動小数点数x
をfrac * 2^exp
の形式に分解します。ここでfrac
は[0.5, 1.0)
の範囲の正規化された仮数部、exp
は整数指数部です。例えば、frexp(12.0)
は(0.75, 4)
を返します(0.75 * 2^4 = 0.75 * 16 = 12
)。ldexp(frac float64, exp int) float64
: この関数はfrac * 2^exp
を計算し、その結果の浮動小数点数を返します。frexp
の逆操作にあたります。
これらの関数は、浮動小数点数のビットレベルでの操作や、特定の範囲での計算精度を維持するために非常に有用です。
累乗の計算アルゴリズム(二分累乗法/バイナリ法)
整数指数 yi
に対する x^yi
の計算には、二分累乗法(Exponentiation by squaring)がよく用いられます。これは、指数を2進数で表現し、そのビットが1である場合にのみ基数を乗算していく方法です。
例えば、x^13
を計算する場合、13
は2進数で 1101
です。
x^13 = x^(8+4+1) = x^8 * x^4 * x^1
この計算は、x
を繰り返し二乗していくことで効率的に行えます。
x^1 -> x^2 -> x^4 -> x^8 -> ...
そして、指数の2進表現の各ビットが1のときに、対応する x^(2^k)
を結果に乗算します。
このコミットのコードでは、for i := int64(yi); i != 0; i >>= 1
ループ内でこの二分累乗法が適用されています。i&1 == 1
で現在のビットが1かどうかをチェックし、x1 *= x1
と xe <<= 1
で x
の二乗と指数部の更新を行っています。
技術的詳細
このコミットの技術的な核心は、math.Pow
関数における x^y
の計算を、x^y = (x^yf) * (x^yi)
のように、y
の小数部 yf
と整数部 yi
に分けて処理するアプローチを維持しつつ、その中間結果の管理方法を改善した点にあります。
元のコードでは、ans
という単一の変数で計算を進め、最後に sys.ldexp(ans, exp)
を呼び出して最終的な浮動小数点数を得ていました。この ans
変数は、Exp(yf * Log(x))
の結果を受け取ったり、x1
を乗算されたりする中で、その値が仮数部と指数部の両方の影響を受ける、やや曖昧な状態でした。
新しいコードでは、a1
と ae
という2つの変数を導入し、常に a1 * 2^ae
という形式で中間結果を表現するようにしました。
-
初期化:
- 変更前:
ans := float64(1);
- 変更後:
a1 := float64(1); ae := 0;
これにより、初期状態が1 * 2^0 = 1
であることが明確になります。
- 変更前:
-
小数部
yf
の処理:- 変更前:
ans = Exp(yf * Log(x));
- 変更後:
a1 = Exp(yf * Log(x));
ここではa1
がx^yf
の結果を受け取ります。この時点ではae
は0
のままなので、a1 * 2^ae
はx^yf
となります。
- 変更前:
-
整数部
yi
の処理(二分累乗法): このループ内で、x
の整数乗x^yi
を計算します。- 変更前:
ans *= x1; exp += xe;
- 変更後:
a1 *= x1; ae += xe;
x1
はsys.frexp(x)
で得られたx
の仮数部、xe
はその指数部です。ループ内でx1
は繰り返し二乗され、xe
は対応して倍増されます。a1
にx1
を乗算し、ae
にxe
を加算することで、a1
は仮数部、ae
は指数部として適切に更新され、常にa1 * 2^ae
の形式が維持されます。これにより、ans
とexp
が別々に更新され、最後に結合されるよりも、計算の各ステップで中間結果の構造が明確になります。
- 変更前:
-
符号反転
flip
の処理:y
が負の場合、最終結果は1 / (x^(-y))
となります。- 変更前:
ans = 1 / ans; exp = -exp;
- 変更後:
a1 = 1 / a1; ae = -ae;
ここでも、a1
とae
がそれぞれ仮数部と指数部として独立して操作されることで、ロジックがより直感的になります。1 / (a1 * 2^ae)
は(1/a1) * 2^(-ae)
となるため、この操作は数学的に正しいです。
- 変更前:
-
最終結果の生成:
- 変更前:
return sys.ldexp(ans, exp);
- 変更後:
return sys.ldexp(a1, ae);
最終的にsys.ldexp
を使用してa1 * 2^ae
を計算し、float64
型の最終結果を返します。
- 変更前:
この変更は、計算の正確性やパフォーマンスに大きな影響を与えるものではなく、主にコードの可読性、保守性、そして浮動小数点数の内部表現をより忠実に反映したロジックへの改善を目的としています。
コアとなるコードの変更箇所
--- a/src/lib/math/pow.go
+++ b/src/lib/math/pow.go
@@ -6,7 +6,7 @@ package math
import "math"
-// x^y: exponentation
+// x^y: exponentiation
export func Pow(x, y float64) float64 {
// TODO: x or y NaN, ±Inf, maybe ±0.
switch {
@@ -38,7 +38,9 @@ export func Pow(x, y float64) float64 {
return Exp(y * Log(x));
}
-\tans := float64(1);\n+\t// ans = a1 * 2^ae (= 1 for now).\n+\ta1 := float64(1);\n+\tae := 0;\n
// ans *= x^yf
if yf != 0 {
if yf > .5 {
@@ -46,42 +48,33 @@ export func Pow(x, y float64) float64 {
yf--;
yi++;
}
-\t\tans = Exp(yf * Log(x));\n+\t\ta1 = Exp(yf * Log(x));\n }\n
// ans *= x^yi
// by multiplying in successive squarings
// of x according to bits of yi.
// accumulate powers of two into exp.
-\t// will still have to do ans *= 2^exp later.\n \tx1, xe := sys.frexp(x);\n-\texp := 0;\n-\tif i := int64(yi); i != 0 {\n-\t\tfor {\n-\t\t\tif i&1 == 1 {\n-\t\t\t\tans *= x1;\n-\t\t\t\texp += xe;\n-\t\t\t}\n-\t\t\ti >>= 1;\n-\t\t\tif i == 0 {\n-\t\t\t\tbreak;\n-\t\t\t}\n-\t\t\tx1 *= x1;\n-\t\t\txe <<= 1;\n-\t\t\tif x1 < .5 {\n-\t\t\t\tx1 += x1;\n-\t\t\t\txe--;\n-\t\t\t}\n+\tfor i := int64(yi); i != 0; i >>= 1 {\n+\t\tif i&1 == 1 {\n+\t\t\ta1 *= x1;\n+\t\t\tae += xe;\n+\t\t}\n+\t\tx1 *= x1;\n+\t\txe <<= 1;\n+\t\tif x1 < .5 {\n+\t\t\tx1 += x1;\n+\t\t\txe--;\n \t\t}\n \t}\n
-\t// ans *= 2^exp\n+\t// ans = a1*2^ae\n \t// if flip { ans = 1 / ans }\n \t// but in the opposite order\n \tif flip {\n-\t\tans = 1 / ans;\n-\t\texp = -exp;\n+\t\ta1 = 1 / a1;\n+\t\tae = -ae;\n \t}\n-\treturn sys.ldexp(ans, exp);\n+\treturn sys.ldexp(a1, ae);\n }\n-\n```
## コアとなるコードの解説
このコミットの主要な変更は、`Pow` 関数の内部ロジックにおける変数の役割の明確化と、それに伴う計算フローの整理です。
1. **変数名の変更と初期化**:
* 変更前は `ans := float64(1);` と単一の `ans` 変数で初期化されていました。
* 変更後は `a1 := float64(1); ae := 0;` と `a1` と `ae` の2つの変数が導入されました。ここで `a1` は結果の仮数部、`ae` は結果の指数部(2のべき乗)を表します。この初期化により、中間結果が常に `a1 * 2^ae` の形式で表現されるという意図が明確になります。
2. **小数部 `yf` の処理**:
* `y` の小数部 `yf` に基づく計算 `Exp(yf * Log(x))` の結果が、以前は `ans` に直接代入されていましたが、変更後は `a1` に代入されます。この時点では `ae` は `0` のままであり、`a1` が `x^yf` の仮数部を保持していることを示します。
3. **整数部 `yi` の処理(二分累乗法ループ)**:
このループは `x^yi` を計算するためのもので、`sys.frexp(x)` で得られた `x` の仮数部 `x1` と指数部 `xe` を利用します。
* 変更前は、`ans *= x1; exp += xe;` のように `ans` と `exp` が別々に更新されていました。
* 変更後は、`a1 *= x1; ae += xe;` となり、`a1` と `ae` がそれぞれ仮数部と指数部として一貫して更新されます。これにより、ループの各イテレーションで `a1 * 2^ae` の形式が維持され、計算の論理がより明確になります。特に、`x1 < .5` の場合の `x1 += x1; xe--;` という正規化のステップも、`a1` と `ae` の関係性を保ちながら行われます。
4. **符号反転 `flip` の処理**:
`y` が負の場合に結果を `1 / result` とする処理も、同様に `a1 = 1 / a1; ae = -ae;` と変更されました。これは `1 / (a1 * 2^ae) = (1/a1) * 2^(-ae)` という数学的関係に基づき、仮数部と指数部をそれぞれ適切に反転させることで、最終的な `ldexp` 呼び出しに備えます。
5. **最終結果の生成**:
最終的に `return sys.ldexp(a1, ae);` を呼び出すことで、`a1` と `ae` を用いて `a1 * 2^ae` を計算し、`float64` 型の最終結果を返します。この変更により、`Pow` 関数の内部状態が `(仮数部, 指数部)` のペアとしてより明確に表現され、コードの意図が向上しました。
全体として、このコミットは `Pow` 関数のアルゴリズム自体を変更するものではなく、その実装における変数の役割と計算フローをより明確にし、コードの可読性と保守性を高めるためのリファクタリングです。
## 関連リンク
* [Go言語の `math` パッケージ](https://pkg.go.dev/math)
* [IEEE 754 浮動小数点数標準](https://ja.wikipedia.org/wiki/IEEE_754)
* [Exponentiation by squaring (二分累乗法)](https://en.wikipedia.org/wiki/Exponentiation_by_squaring)
## 参考にした情報源リンク
* コミット情報: `/home/orange/Project/comemo/commit_data/1205.txt`
* GitHubコミットページ: [https://github.com/golang/go/commit/2e7e76073adeafa07b444ea673507a974659831f](https://github.com/golang/go/commit/2e7e76073adeafa07b444ea673507a974659831f)
* Go言語の `math` パッケージのドキュメント (現在のバージョン): [https://pkg.go.dev/math](https://pkg.go.dev/math)
* 浮動小数点数に関する一般的な知識 (IEEE 754など)
* 二分累乗法に関する一般的なアルゴリズム知識