【Rust自作TUI電卓】ニュートン法×マジックナンバーで実装する汎用n乗根 #13

n-th-rootを効率的に求める計算手法。rust電卓第13回.

自作TUI関数電卓制作12回である前回は、平方根を実装しました。今回は、立方根やそれ以上の乗数の根号の計算を実装していこうと思います。
前回 平方根を求める:【Rust自作TUI電卓】mod.rsを活用したスマートなモジュール管理と基本数学関数の実装 #11

1. 漸化式を求める

前回使ったニュートン法で、漸化式を求めてみると、以下のようになりました。
バビロニア法・ニュートン法平方根の導出展開、手動でも解ける差分アプローチ【バビロニア法・ニュートン法】

立方根(3乗根)の場合: \(x = \sqrt[3]{X}\)

立方根を求める関数を \(f(x) = x^3 – X\) とした時の導出プロセスです。
\[x_{new} = \frac{3x^3 – (x^3 – X)}{3x^2}\]
\[x_{new} = \frac{2x^3 + X}{3x^2}\]
\[x_{new} = \frac{1}{3} \left( 2x + \frac{X}{x^2} \right)\]


n 乗根の場合: \(x = \sqrt[n]{X}\)

任意の正の整数 \(n\) における一般的なニュートン法の形式です。
\[x_{new} = \frac{nx^n – (x^n – X)}{nx^{n-1}}\]
\[x_{new} = \frac{(n-1)x^n + X}{nx^{n-1}}\]
\[x_{new} = \frac{1}{n} \left( (n-1)x + \frac{X}{x^{n-1}} \right)\]

2. ニュートン法で実装

以下のように実装しました。


pub fn custom_cbrt(x: f64) -> f64 {
    if x == 0.0 {
        return 0.0;
    }

    let mut guess = x / 3.0; // 初期値は立方根の1/3
    let mut prev_guess;

    for _ in 0..20 {
        prev_guess = guess;
        guess = (2.0 * guess + x / (guess * guess)) / 3.0; // ニュートン法の更新式
        if (guess - prev_guess).abs() < f64::EPSILON {
            break;
        }
    }
    guess
}

pub fn custom_root_n(x: f64, n: u32) -> f64 {
    // エッジケースの処理
    if n == 0 { return f64::NAN; }
    if n == 1 { return x; }
    if x == 0.0 { return 0.0; }

    // 負の数に対する処理
    if x < 0.0 {
        if n % 2 == 0 {
            return f64::NAN; // 偶数乗根は実数解なし
        } else {
            // 奇数乗根なら絶対値で計算して最後に符号を戻す
            return -custom_root_n(-x, n);
        }
    }

    let n_f = n as f64;
    // 初期値の推定。ニュートン法準拠
    let mut guess = x / n_f; 
    let mut prev_guess;

    for _ in 0..30 { 
        prev_guess = guess;
        let x_pow_n_minus_1 = guess.powi(n as i32 - 1);
        guess = ((n_f - 1.0) * guess + x / x_pow_n_minus_1) / n_f;
        if (guess - prev_guess).abs() < f64::EPSILON {
            break;
        }
    }
    guess
}

2.1 ベンチマークと精度テスト

サンプルとして、わかりやすい \(2^6 = 64\)をそれぞれの根号で計算してみました。

画像 ニュートン法実行結果

動作結果を見ると、5乗根などはとても遅くなってしまっていることが確認できます。

3. 改良 初期値をマジックナンバーで処理する

上記の実装では、初期値をニュートン法に準拠して\( 1/n\)としていましたが、これでは、大きな数を用いた時などにおいてnが大きくなればなるほど漸化式の2項目が大きくなり、収束に時間がかかりやすいという問題があります。

\[\frac{X}{x^{n-1}}\]
x = 1.0を愚直に計算しようとしたら、初期値は\(1.0 / 6 \approx 0.166…\) になりますね。
\[x_{n+1} = \frac{5 \times 0.166 + (1.0 / 0.166^5)}{6}\]
ここで \(0.166^5\) は非常に小さな数(約 \(0.000128\))なので、その逆数は 7776 という巨大な数になります。最終的な回答は1.0になるはずなのに、こんなに大きな数に飛んでしまうと収束に時間がかかってしまいます。

実際に、計算回数(収束のためのループ)を30回に制限して計算すると以下のようになり、30回回しても6乗根では、f64の仮数部における精度を十分に満たすまで計算できていないことが確認できます。

収束しきれないので、6乗根が1になっていない

画像:1だと逆に遅くなる

3.1 改良手段 高速逆平方根

前回と同様に、逆平方根を用いた方法を使ってみようと思います。

n 乗根を直接求める「魔法の数字」の数式

IEEE 754形式(f64)において、\(y = x^{1/n}\) をビット演算で近似する一般式は以下の通りです。この式の導出をここですると長くなるので、【ルート高速計算の仕組み】n乗根ビットハックの導出プロセスにまとめました。興味があれば見てみてください。

この数式から導き出した、各 \(n\) 乗根用の「魔法の数字(加算用定数)」は以下の通りです。
他の \(n\) についても、定数項\[K_n \approx -\frac{1}{2} I_x + \frac{3}{2} L(B – \sigma)\] を計算しました。これらを使えば、任意の \(n\) 乗逆根の高速な初期値を求めることができます。

n 乗根計算式 (整数演算)魔法の数字 (16進数)備考
2乗逆根K - (i / 2)0x5fe6eb50c7b537a9\(1/\sqrt{x}\)
3乗逆根K - (i / 3)0x554d42f7ccc8b892\(1/\sqrt[3]{x}\)
4乗逆根K - (i / 4)0x4ff86ec350422d0a\(1/\sqrt[4]{x}\)
5乗逆根K - (i / 5)0x4cc7ee03cfec1209\(1/\sqrt[5]{x}\)
6乗逆根K - (i / 6)0x4aa79684347100b2\(1/\sqrt[6]{x}\)

魔法の数字の導出 : 【ルート高速計算の仕組み】n乗根ビットハックの導出プロセス

4. 計算した魔法の数字を使い、高速化

cbrt(立方根)はよく使うと思われるので、個別で実装し、それ以上(2や3も対応しているが早いcbrtなどを呼び出す形)はrootという関数で実装してみました。

// --- 3乗根専用 ---
pub fn custom_cbrt(x: f64) -> f64 {
    if x == 0.0 { return 0.0; }
    if x < 0.0 { return -custom_cbrt(-x); }

    let i = x.to_bits();
    let mut guess = f64::from_bits((i / 3) + 0x2a9f4795cfce2000);

    // ニュートン法
    for _ in 0..3 {
        let g2 = guess * guess;
        guess = (2.0 * guess + x / g2) / 3.0;
    }
    guess
}

// --- 汎用n乗根 ---
pub fn custom_root_n(x: f64, n: u32) -> f64 {
    // エッジケース
    if n == 0 { return f64::NAN; }
    if n == 1 { return x; }
    if x == 0.0 { return 0.0; }
    if x < 0.0 {
        if n % 2 == 0 { return f64::NAN; }
        else { return -custom_root_n(-x, n); }
    }

    // ステップ1: nに応じて初期値を推定
    let mut guess = match n {
        2 => sqrt(x)
        3 => custom_cbrt(x),
        4 => {
            let i = x.to_bits();
            f64::from_bits((i / 4) + 0x2ff6f08899d39000)
        },
        5 => {
            let i = x.to_bits();
            f64::from_bits((i / 5) + 0x3325da722c26d000)
        },
        6 => {
            let i = x.to_bits();
            f64::from_bits((i / 6) + 0x354760eed2f65000)
        },
        _ => {
            // 7乗根以上:定数を動的に計算
            let i = x.to_bits();
            let n_f = n as f64;
            let sigma = 0.0430; // 補正係数(大体の値なので最適化はできてない)
            let bias = 1023.0;
            let l = (1u64 << 52) as f64;
            
            let magic = (((n_f - 1.0) / n_f) * (bias - sigma) * l) as u64;
            f64::from_bits((i / n as u64) + magic)
        }
    };

    // すでに cbrt などで計算済みの場合はそのまま返す
    if n <= 3 { return guess; }

    // ニュートン法
    let n_f = n as f64;
    for _ in 0..5 {
        let prev_guess = guess;
        let x_pow_n_minus_1 = guess.powi(n as i32 - 1);
        if x_pow_n_minus_1 == 0.0 { break; }
        
        guess = ((n_f - 1.0) * guess + x / x_pow_n_minus_1) / n_f;

        if (guess - prev_guess).abs() < f64::EPSILON {
            break;
        }
    }
    guess
}

計算結果

計算結果を見ると、5thや6thでは、シンプルなニュートン法だと400msかかっていたものが70ms程度になっており、5倍以上の高速化ができていることが確認できる。

N乗根の計算を逆根を用いて高速化した結果

cbrtはまだ進化を残している。

今回の実装で、多乗根についての計算が比較的高速に行えるようになりました。立方根のcbrtについては、場合によっては、rustの標準で導入されているcbrtよりも早かったりします。(標準cbrt は14 ~ 18ms 今回実装したものは 15~18程度で、数によっては自作の方が早かったりしました)

マジックナンバーを用いることで高速化できることは確認できましたが、基本使っているのはニュートン法という2次で収束する手法でした。実は3次収束する手法もあり、立方根についてはこの方が最適になることがあるようです。

次回は立方根をハレー法で収束させる実験をしてみてどれが早いのか検証していこうと思います。

次回平方根の導出展開、手動でも解ける差分アプローチ【バビロニア法・ニュートン法】

ここまで読んでいただきありがとうございます。

では、次の記事で。 lumenHero

関連記事

第1回 構想編【Rustで作る】ターミナル関数電卓を設計する(アーキテクチャ編) #1
第2回 Lexer実装:【Rust】自作計算機エンジンへの道:Lexer(字句解析)で数式をトークンに分解する #2
第3回 tomlの読み込み:【Rust】TOMLで関数を自由に追加!自作電卓に設定読み込みとホットリロードを実装する #3
第4回 Parserの実装(基礎):【Rust】数式を「計算の木」へ:ASTと再帰下降構文解析で電卓エンジンの心臓部を作る #4
第6回 比較演算子の実装 :【Rust】電卓に「論理」を。比較演算子の実装と優先順位の階層設計 #6
第7回 Evaluatorの実装 :【Rust】計算機自作:演算と比較を評価するEvaluatorの実装(式指向の設計) #7
第11回 平方根の計算実装:【Rust自作TUI電卓】mod.rsを活用したスマートなモジュール管理と基本数学関数の実装 #11
バビロニア法・ニュートン法平方根の導出展開、手動でも解ける差分アプローチ【バビロニア法・ニュートン法】