package main import "fmt" func main() { var k int var p, t, last float64 p = 0 k = 1 t = 16.0 / 5.0 for { last = p p += t / float64(k) t /= -5.0 * 5.0 k += 2 if p == last { break } } k = 1 t = 4.0 / 239.0 for { last = p p -= t / float64(k) t /= -239.0 * 239.0 k += 2 if p == last { break } } fmt.Println(p) }
3.141592653589794
丸め誤差で最後の桁はおかしくなってる
追記
AIで丸め誤差を解析させた
Machin の公式によるπ計算 — 丸め誤差の蓄積過程
main.go は Machin の公式
π = 4 · ( 4·arctan(1/5) − arctan(1/239) )
を、arctan のテイラー級数(交代級数)で float64 のまま計算している。
- フェーズ1:
t = 16/5から始めて4·arctan(1/5)を求める(main.go13〜21行目) - フェーズ2:
t = 4/239から始めてarctan(1/239)を引く(main.go22〜32行目) - どちらのループも
p == last(1つ前の値から変化がなくなった)で打ち切る
各ステップで「次に加える項 t/k」と「その時点の p と math.Pi との差」を記録すると、
どこまでが級数の数学的な収束で、どこから float64 の丸め誤差が支配的になるかが分かる。
ステップごとのデータ
| phase | step | k | term (加える項) | p | diff = p − π |
|---|---|---|---|---|---|
| 1 | 1 | 1 | 3.2 | 3.2 | 5.840734641020706e-2 |
| 1 | 2 | 3 | -4.2666666666666665e-2 | 3.1573333333333333 | 1.574067974354021e-2 |
| 1 | 3 | 5 | 1.0240000000000002e-3 | 3.1583573333333335 | 1.6764679743540345e-2 |
| 1 | 4 | 7 | -2.925714285714286e-5 | 3.158328076190476 | 1.673542260068306e-2 |
| 1 | 5 | 9 | 9.102222222222223e-7 | 3.158328986412698 | 1.6736332822905098e-2 |
| 1 | 6 | 11 | -2.978909090909091e-8 | 3.1583289566236075 | 1.6736303033814348e-2 |
| 1 | 7 | 13 | 1.008246153846154e-9 | 3.158328957631854 | 1.6736304042060723e-2 |
| 1 | 8 | 15 | -3.4952533333333335e-11 | 3.1583289575969014 | 1.6736304007108238e-2 |
| 1 | 9 | 17 | 1.2336188235294119e-12 | 3.158328957598135 | 1.6736304008341918e-2 |
| 1 | 10 | 19 | -4.415056842105264e-14 | 3.158328957598091 | 1.6736304008297953e-2 |
| 1 | 11 | 21 | 1.5978300952380953e-15 | 3.158328957598093 | 1.673630400829973e-2 |
| 1 | 12 | 23 | -5.835553391304349e-17 | 3.158328957598093 | 1.673630400829973e-2 |
| 2 | 13 | 1 | -1.6736401673640166e-2 | 3.141592555924453 | -9.76653402595673e-8 |
| 2 | 14 | 3 | 9.76663671483352e-8 | 3.14159265359082 | 1.0267342531733448e-12 |
| 2 | 15 | 5 | -1.0258892576985893e-12 | 3.141592653589794 | 8.881784197001252e-16 |
| 2 | 16 | 7 | 1.2828522631138778e-17 | 3.141592653589794 | 8.881784197001252e-16 |
最終値: p = 3.14159265358979400418(math.Pi = 3.14159265358979311600、diff = 8.881784e-16)
読み方
- フェーズ1(step 1〜12): 項は 25 倍ずつ縮み、
pは4·arctan(1/5) ≈ 3.158328957598093に収束する。 ここでのdiff(≈0.0167)は丸め誤差ではなく、まだarctan(1/239)の補正をしていないことによる数学的なズレ。 - フェーズ2(step 13〜16): 補正項を引くことで
diffは急激に縮む(1e-8 → 1e-12 → 1e-16)。 - 丸め誤差が顔を出す境目: step 15〜16 で「加える項」が
pの ULP(float64 の1刻み、p ≈ 3.14なら約2.22e-16 × p ≈ 7e-16)を下回る。 そうなるとp += termは浮動小数点演算上pを全く変化させなくなり、p == lastが成立してループが終了する。 - 最終誤差
8.88e-16(≈4 ULP) が、この計算で約16回の加減算を重ねた結果として蓄積した丸め誤差そのもの。 数学的には項をさらに足せばπにもっと近づくはずだが、float64 の精度(約15〜17桁)がそこで頭打ちになり、 それ以上は「級数を計算している」のではなく「丸め誤差の中で足踏みしている」状態になる。
参考: 計測に使ったコード
main.go の各ループに以下の計装を加えて実行した(本体のロジックは変更していない)。
term := t / float64(k) p += term // または p -= term // term, p, p-math.Pi を記録
