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

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

このコミットは、Go言語の標準ライブラリである math パッケージ内の Hypot 関数に関するものです。math パッケージは、浮動小数点数に対する基本的な数学関数を提供しており、Hypot(x, y) 関数は、直角三角形の斜辺の長さを計算する、すなわち sqrt(x*x + y*y) を計算する機能を提供します。この関数は、数値計算において、引数の値が非常に大きい場合や小さい場合に発生しうるオーバーフローやアンダーフローの問題を避けるように設計されていることが一般的です。

コミット

このコミットは、math パッケージ内に存在していた2つの Hypot 実装のうち、Sqrt 関数に基づかない(そして不正確であった)実装を削除し、より正確な Sqrt 関数に基づく実装に統一することを目的としています。これにより、特にARMアーキテクチャにおけるビルドの問題が修正されました。

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

https://github.com/golang/go/commit/2572803899485e6d07490ae04b1eb5aa5c758699

元コミット内容

math: delete non-Sqrt-based Hypot

I was confused by the existence of two portable Hypot
routines in the tree when I cleaned things up, and I made
ARM use the wrong (imprecise) one. Use the right one,
and delete the wrong one.

Fixes arm build.

R=golang-dev, r
CC=golang-dev
https://golang.org/cl/5485065

変更の背景

Go言語の math パッケージには、Hypot 関数を計算するための2つの異なる内部ルーチンが存在していました。一つは Sqrt 関数(平方根)を内部的に利用する hypotSqrt であり、もう一つは Sqrt を利用しない hypotNoSqrt でした。コミットメッセージによると、コミッターであるRuss Cox氏は、これらの2つのポータブルな Hypot ルーチンが共存していることに混乱し、クリーンアップの過程でARMアーキテクチャが誤って「不正確な」hypotNoSqrt を使用するように設定してしまったと述べています。

Hypot 関数の計算は、単純に sqrt(x*x + y*y) とすると、xy が非常に大きい場合に x*xy*y が浮動小数点数の最大値を超えてオーバーフローしたり、逆に非常に小さい場合にアンダーフローして精度が失われたりする可能性があります。これを避けるために、通常は max(|x|, |y|) * sqrt(1 + (min(|x|, |y|) / max(|x|, |y|))^2) のようなスケーリングされた形式で計算されます。このスケーリングされた形式は Sqrt 関数に依存します。

hypotNoSqrt は、おそらく Sqrt 関数が利用できない、あるいは非常に遅い環境のために、反復計算や級数展開などを用いて Hypot を近似的に計算しようとしたものと推測されます。しかし、このような実装は、特定の条件下で精度が劣る("imprecise")か、あるいは計算コストが高い可能性があります。

このコミットの目的は、不正確な hypotNoSqrt を削除し、より正確で標準的な Sqrt ベースの Hypot 実装に統一することで、コードベースの簡素化と、特にARMアーキテクチャにおける計算の正確性を保証することでした。結果として、ARMビルドの問題も解決されました。

前提知識の解説

Hypot関数 (Hypotenuse Function)

Hypot(x, y) は、数学的には sqrt(x^2 + y^2) を計算する関数です。これは、直角三角形の2つの直角を挟む辺の長さ xy から斜辺の長さを求める際に使用されます。また、2次元平面上の点 (x, y) と原点 (0, 0) との間のユークリッド距離を求める際にも用いられます。

浮動小数点数の精度とオーバーフロー/アンダーフロー

コンピュータにおける浮動小数点数(float32, float64など)は、限られたビット数で実数を表現するため、常に精度に限界があります。特に、非常に大きな数や非常に小さな数を扱う際には、以下の問題が発生する可能性があります。

  • オーバーフロー (Overflow): 計算結果がそのデータ型で表現できる最大値を超えてしまうこと。例えば、x が非常に大きい場合、x*xfloat64 の最大値 (MaxFloat64) を超えて +Inf (無限大) になってしまうことがあります。
  • アンダーフロー (Underflow): 計算結果がそのデータ型で表現できる最小の非ゼロ値よりも小さくなってしまい、ゼロとして扱われること。例えば、x が非常に小さい場合、x*x はゼロとして扱われてしまい、精度が失われることがあります。

Hypot(x, y) を単純に sqrt(x*x + y*y) と計算すると、xy が大きい場合に x*xy*y がオーバーフローし、結果が Inf になってしまう可能性があります。また、xy が小さい場合に x*xy*y がアンダーフローし、結果がゼロに近づきすぎて精度が失われる可能性があります。

Hypotの安定した実装戦略

これらの問題を回避するため、Hypot 関数は通常、以下のようなスケーリングされた形式で実装されます。

  1. a = abs(x), b = abs(y) とする。
  2. ab のうち大きい方を p、小さい方を q とする(p = max(a, b), q = min(a, b))。
  3. もし p がゼロであれば、結果はゼロ。
  4. そうでなければ、result = p * sqrt(1 + (q/p)^2) を計算する。

この方法では、q/p は常に1以下になるため、(q/p)^2 は1以下となり、1 + (q/p)^2 は2以下となります。これにより、中間計算でのオーバーフローやアンダーフローが効果的に回避され、より広い範囲の入力値に対して正確な結果が得られます。この実装は Sqrt 関数に依存します。

ARMアーキテクチャ

ARM (Advanced RISC Machine) は、組み込みシステム、モバイルデバイス、サーバーなど、幅広いデバイスで使用されているRISC (Reduced Instruction Set Computer) ベースのプロセッサアーキテクチャです。ARMプロセッサは、その電力効率の高さと性能のバランスから広く採用されています。

特定のアーキテクチャ(この場合はARM)が、なぜ特定の Hypot 実装を使用するように設定されていたのかは、そのアーキテクチャの浮動小数点演算ユニット (FPU) の特性や、特定の命令の性能特性に起因する可能性があります。例えば、過去にはハードウェアによる sqrt 命令が遅い、あるいは存在しない環境のために、ソフトウェアによる代替実装が検討されることがありました。しかし、現代のARMプロセッサは通常、効率的なFPUと sqrt 命令を備えています。

技術的詳細

このコミットの核心は、src/pkg/math/hypot.go ファイル内の2つの Hypot 実装、hypotSqrthypotNoSqrt のうち、hypotNoSqrt を削除し、hypotSqrthypot にリネームして標準の実装とすることです。

  • hypotSqrt (リネーム後 hypot): この関数は、前述の安定した実装戦略、すなわち p * Sqrt(1+q*q) の形式で Hypot を計算します。

    func hypot(p, q float64) float64 {
        // ... (特殊ケースのハンドリング: NaN, Inf)
        if p < 0 {
            p = -p
        }
        if q < 0 {
            q = -q
        }
    
        if p < q {
            p, q = q, p // pを大きい方、qを小さい方にする
        }
    
        if p == 0 {
            return 0
        }
    
        q = q / p // q/p を計算
        return p * Sqrt(1+q*q) // p * sqrt(1 + (q/p)^2)
    }
    

    この実装は、浮動小数点数のオーバーフロー/アンダーフローを効果的に回避し、高い精度を維持します。

  • hypotNoSqrt (削除された関数): この関数は、Sqrt 関数を使用せずに Hypot を計算しようとするものでした。コミット前のコードを見ると、これは反復的な計算を行っていました。具体的なアルゴリズムはコードから読み取れますが、おそらくはテイラー級数展開やニュートン法のような数値解析的手法を用いて sqrt(1+x) を近似的に計算し、それを p に掛けることで Hypot を求めていたと考えられます。

    func hypotNoSqrt(p, q float64) float64 {
        // ... (特殊ケースのハンドリング: NaN, Inf)
        // ... (p, q の絶対値を取り、pを大きい方、qを小さい方にする)
        if p == 0 {
            return 0
        }
    
        pfac := p
        q = q / p
        r := q
        p = 1
        for {
            r = r * r
            s := r + 4
            if s == 4 { // rが非常に小さい場合、sは4に収束
                return p * pfac
            }
            r = r / s
            p = p + 2*r*p
            q = q * r
            r = q / p
        }
        // panic("unreachable")
    }
    

    この反復計算は、特定の条件下で収束が遅い、あるいは丸め誤差が蓄積しやすいなど、数値的な不安定性や「不正確さ」を抱えていた可能性があります。コミットメッセージの「imprecise」という言葉は、この数値的な特性を指していると考えられます。

ARMアーキテクチャがこの hypotNoSqrt を使用するように設定されていたため、ARMビルドで問題が発生していました。これは、hypotNoSqrt の不正確さが、特定のテストケースや実運用環境で許容できない誤差を生じさせていたか、あるいは単にパフォーマンスが劣っていたためかもしれません。このコミットにより、ARMも他のアーキテクチャと同様に、より正確で安定した Sqrt ベースの Hypot 実装を使用するようになりました。

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

このコミットによる主要なコード変更は以下のファイルに集中しています。

  1. src/pkg/math/all_test.go:

    • TestHypotSqrtGoTestHypotNoSqrtGo という2つのテスト関数が削除され、TestHypotGo という単一のテスト関数に統合されました。
    • 同様に、ベンチマーク関数 BenchmarkHypotNoSqrtGoBenchmarkHypotSqrtGo が削除され、BenchmarkHypotGo に統合されました。
    • これにより、Hypot 関数のテストが、単一の正確な実装に対して行われるようになりました。
  2. src/pkg/math/export_test.go:

    • テストのために内部関数をエクスポートするファイルです。
    • HypotSqrtGoHypotNoSqrtGo のエクスポートが削除され、代わりに HypotGo がエクスポートされるようになりました。これは、内部実装が hypot に統一されたことを反映しています。
  3. src/pkg/math/hypot.go:

    • hypotNoSqrt 関数が完全に削除されました。
    • hypotSqrt 関数が hypot にリネームされました。これにより、Hypot 関数のGo言語によるデフォルト実装が Sqrt ベースのものに統一されました。
  4. src/pkg/math/hypot_arm.s:

    • ARMアーキテクチャ向けのアセンブリ言語による Hypot 関数の実装(またはラッパー)です。
    • 以前は hypotNoSqrt を呼び出すように設定されていましたが、このコミットにより hypot を呼び出すように変更されました。
    • 具体的には、B ·hypotNoSqrt(SB)B ·hypot(SB) に変更されています。これは、ARMアーキテクチャがGoの標準的な Sqrt ベースの Hypot 実装を使用するようになったことを意味します。

コアとなるコードの解説

このコミットの目的は、math.Hypot 関数の実装を簡素化し、正確性を向上させることです。

  • hypotNoSqrt の削除: Sqrt を使用しない Hypot の実装は、おそらく特定の環境でのパフォーマンスや、Sqrt が利用できない場合のフォールバックとして存在していたと考えられます。しかし、コミットメッセージが示唆するように、この実装は「不正確」であり、現代のGoの実行環境では Sqrt が効率的に利用できるため、その存在意義が薄れていました。不正確な実装を削除することで、コードベースの複雑性が減り、将来的なメンテナンスが容易になります。

  • hypotSqrt から hypot へのリネーム: hypotSqrt は、Hypot 関数の安定した計算方法(p * Sqrt(1 + (q/p)^2))を実装していました。これを hypot にリネームすることで、この実装が math.Hypot の標準的かつ唯一のGo言語による実装であることを明確にしています。

  • hypot_arm.s の変更: ARMアーキテクチャが hypotNoSqrt を使用していたことが、このコミットの直接的なトリガーの一つでした。ARMアセンブリコードが hypotNoSqrt から hypot へと呼び出し先を変更したことで、ARM環境でも正確な Sqrt ベースの Hypot 計算が保証されるようになりました。これにより、「Fixes arm build」というコミットメッセージの目的が達成されました。これは、ARMビルドが hypotNoSqrt の不正確さに起因するテスト失敗や誤動作を起こしていた可能性を示唆しています。

全体として、このコミットは、Goの math パッケージにおける Hypot 関数の実装を、より堅牢で正確な単一のバージョンに統一し、特定のアーキテクチャにおける潜在的な問題を解決する、クリーンアップと品質向上を目的とした変更です。

関連リンク

参考にした情報源リンク

  • IEEE 754 浮動小数点数標準に関する情報 (浮動小数点数の精度、オーバーフロー、アンダーフローの概念理解のため)
  • Hypot 関数の数値的に安定した実装に関する情報 (例: Wikipedia, 数値計算の教科書)
  • ARMアーキテクチャの浮動小数点演算に関する一般的な情報
  • Go言語のソースコードとコミット履歴 (Goの内部実装の理解のため)