第5章: 高速除算アルゴリズム

難易度: 中級

Fast Division Algorithms — Knuth D, Burnikel-Ziegler, and Newton Iteration

多倍長整数の除算は、乗算より本質的に難しい。乗算はすべての桁の積和で定まるため並列化しやすいが、 除算は「商の各桁を推測し、間違っていたら修正する」という逐次的な構造を持つからである。 高度に最適化された実装でも、除算は乗算の 2〜5 倍遅いのが一般的である。

本章では、学校式除算を体系化した Knuth Algorithm D から始めて、 分割統治による再帰除算 (Burnikel-Ziegler)、 除算を乗算に還元する Newton 逆数反復、 余りが $0$ と分かっているときの特殊実装 Exact Division まで扱う。 sangi の実装を参照しつつ、サイズごとにどのアルゴリズムが最速かを定量的に把握する。

5.1 学校式除算 (Knuth Algorithm D)

$n$ ワードの被除数 $A = (a_{n-1}, \ldots, a_0)$ を $m$ ワードの除数 $B = (b_{m-1}, \ldots, b_0)$ で割り、 商 $Q$ と余り $R$ ($A = QB + R$, $0 \le R < B$) を求める。 Knuth の The Art of Computer Programming Vol. 2 §4.3.1 に記述される Algorithm D は、すべての高速除算アルゴリズムの土台である。

Algorithm D の概要

  1. 正規化: $b_{m-1}$ の最上位ビットが $1$ になるよう $A, B$ を同じ量だけ左シフト。
  2. 反復 ($j = n-m, n-m-1, \ldots, 0$):
    1. 試しの商 $\hat{q}$ を上位 2 ワードから推測する。
    2. $A \leftarrow A - \hat{q} \cdot B \cdot \beta^j$ を計算 ($\beta = 2^{64}$)。
    3. 負になったら $\hat{q} \leftarrow \hat{q} - 1$、$A \leftarrow A + B \cdot \beta^j$ で補正。
    4. $q_j \leftarrow \hat{q}$ を記録。
  3. 脱正規化: 余りを右シフトで元に戻す。

計算量は $O(nm)$ 回のワード乗算である。$n = m$ のバランス除算では $O(n^2)$、 これは学校で習う筆算と本質的に同じ計算手順である。

なぜ正規化が必要か

$b_{m-1}$ の最上位ビットが $1$ のとき、試しの商の推測誤差が必ず $-2$ から $0$ の範囲に収まる (Knuth Theorem B)。したがって補正は多くとも 2 回で済み、 平均的には $10^{-19}$ 程度の確率でのみ発生する。 正規化なしでは推測誤差が大きくなり、補正が多発して性能が劣化する。

sangi では MpnOps.hppdiv_basecase / sbpi1_div_qr がこの役割を担う。基本構造は次の通りである。

// MpnOps.hpp の div_basecase (概略)
inline void div_basecase(uint64_t* q, uint64_t* a, size_t an,
                         const uint64_t* b, size_t bn) {
    const uint64_t d1 = b[bn - 1];   // MSB = 1 (正規化済み)
    const uint64_t d0 = b[bn - 2];
    // 事前計算: dinv = ⌊(β² − 1) / d1⌋ − β  (Möller-Granlund)
    // 反復: 各位置 j で 3-by-2 商推測 + addmul_1 による減算 + 補正
}

5.2 商の推測 — 2-by-1 と 3-by-2

Algorithm D の内ループは「$A$ の上位 3 ワードと $B$ の上位 1〜2 ワードから $\hat{q}$ を推測する」ことに尽きる。 計算時間のほとんどはこのワード単位の除算に費やされるため、この微小な操作の最適化が全体性能を左右する。

2-by-1 の推測

最も単純な推測は上位 2 ワードからの商である:

$$\hat{q} = \min\!\left(\left\lfloor \dfrac{u_{n+1}\beta + u_n}{v_{n-1}} \right\rfloor,\; \beta - 1\right).$$

これは 1 回のハードウェア除算 (DIV 命令) で計算できる。x86-64 の DIV r64 は 128-by-64 bit 除算を約 $20\text{-}30$ サイクルで実行する。

3-by-2 の推測 (Möller-Granlund)

より精度の高い推測として、上位 3 ワードを用いる方法がある。 Möller & Granlund (2011) は、事前計算した逆数 $\text{dinv}$ を使って、 ハードウェア除算を完全に除去した 3-by-2 推測を提示した:

$$\text{dinv} = \left\lfloor \dfrac{\beta^2 - 1}{d_1} \right\rfloor - \beta, \quad (q_1, q_0) = u_2 \cdot \text{dinv} + (u_2, u_1).$$

導出の詳細:$\text{dinv}$ の式はどこから来るか

Step 1(目標):$d_1 \in [\beta/2, \beta)$(正規化済み)に対し、$\beta$-進で $1/d_1$ を表したい。$1/d_1 \in (1/\beta, 2/\beta]$ なので、スケールを $\beta^2$ にして整数化する。

Step 2($\beta^2/d_1$ の整数部):定義より $V := \lfloor (\beta^2 - 1)/d_1 \rfloor$ は $\beta^2/d_1$ の床(境界条件で $\beta^2 - 1$ を使うのは $V \cdot d_1 \le \beta^2 - 1 < \beta^2$ を保証するため)。$V \in [\beta, 2\beta)$ で常に 1 ワード溢れる。

Step 3(オフセット):$\text{dinv} := V - \beta$ とおけば $\text{dinv} \in [0, \beta)$ で 1 ワードに収まる。実装上は「$\beta$ 進の $1.\text{dinv}$ が $\beta/d_1$ の近似」と読める。

Step 4(推測式):商推測 $\hat{q} = u_2 \cdot V / \beta = u_2 + u_2 \cdot \text{dinv}/\beta$ の高位部分を取り出すと、上記の $(q_1, q_0)$ 加算式になる。誤差 $\hat{q} - q \in \{0, 1\}$ は Möller-Granlund Theorem 1。$\square$

この MULX + 加算で $\hat{q}$ の推測が $2\text{-}3$ サイクルで済む。 事前計算した $\text{dinv}$ は除数が変わらない限り再利用できるため、長い除算では圧倒的に有利である。

// sangi: 3-by-2 商推測 (MpnOps.hpp より)
inline void udiv_qrnnd_3by2(uint64_t& q, uint64_t& r1, uint64_t& r0,
                            uint64_t u2, uint64_t u1, uint64_t u0,
                            uint64_t d1, uint64_t d0, uint64_t dinv) {
    uint64_t p_hi;
    uint64_t p_lo = _umul128(u2, dinv, &p_hi);
    uint64_t q0 = p_lo + u1;
    uint64_t q1 = p_hi + u2 + (q0 < p_lo);
    // 以下: 商の条件付き補正 (多くとも 2 回)
}

推測誤差の上界

$\text{dinv} = \lfloor (\beta^2 - 1)/d_1 \rfloor - \beta$ は、言わば 「$d_1$ の逆数を $\beta$-進で $1 + \text{dinv}/\beta$ 近似した」形である。 これを 3 ワードの被除数と積むと、真の商との誤差が 3 ワード目のキャリー相当の狭い範囲に収まる。 正規化済み $B$ に対する 3-by-2 推測の誤差 $\hat{q} - q$ は $\{0, 1\}$ のみで、正規化の効果が効く (Möller-Granlund, Theorem 1)。 2-by-1 推測の $\{0, 1, 2\}$ より狭く、補正ループが簡潔になる。

5.3 Burnikel-Ziegler 再帰除算

Knuth D は $O(n^2)$ のワード乗算を要する。 サイズが数百 ワードに達すると、乗算を内部で高速化しても全体が $O(n^2)$ に縛られる。 Burnikel & Ziegler (1998) は、分割統治により除算を $O(M(n) \log n)$ に落とした。 ここで $M(n)$ は $n$ ワード乗算のコスト (Karatsuba 以降の高速乗算が使える)。

Div-2n-by-n パターン

被除数 $A$ が $2n$ ワード、除数 $B$ が $n$ ワードで、$A / B$ の商が $n$ ワードに収まる場合を考える。 $B$ と $A$ をそれぞれ $n/2$ ワード単位で分割する:

$$B = B_1 \beta^{n/2} + B_0, \quad A = A_3 \beta^{3n/2} + A_2 \beta^{n} + A_1 \beta^{n/2} + A_0.$$

このとき、商を上下 2 つに分けて順に再帰的に求めることができる:

  1. 上半分の商 $Q_1$: $A$ の上位 $3n/2$ ワードを $B$ で割る (再帰 Div-$3n$/2-by-$n/2$)。
  2. 下半分の商 $Q_0$: 更新された中間余りと $A$ の下半分から再帰で求める。
  3. 結合: $Q = Q_1 \beta^{n/2} + Q_0$。

再帰の各ステップで、サイズが半分の除算と、その結果を使った 1 回の $n/2 \times n$ 乗算が発生する。乗算コストを $M(n)$ とすると:

$$D(2n, n) = 2\, D(n, n/2) + O(M(n)).$$

導出の詳細:再帰の正当性

Step 1(上半分 $Q_1$):$A$ の上位 $3n/2$ ワード $A_{\text{hi}} = A_3 \beta^{n/2} + A_2 + A_1 \beta^{-n/2}$(実質 $A_3 \beta^n + A_2 \beta^{n/2} + \cdots$)を $B$ で割る再帰呼び出しから商 $Q_1$ と部分余り $R_1$ が得られる。

Step 2(補正と中間余り):$Q_1$ を求めた時点で、まだ $A$ の下半分は処理していない。新しい被除数を

$$\displaystyle A' = R_1 \cdot \beta^{n/2} + A_0$$

とおく。$A' < B \cdot \beta^{n/2}$ が再帰の前提条件で、$Q_1$ を $A_{\text{hi}}/B$ の真の商に揃えることでこれを満たす(補正は最大 2 回の $B$ 加減算)。

Step 3(下半分 $Q_0$ と結合):$A'$ を $B$ で割って $Q_0$, $R$ を得る。すると

$$\displaystyle A = Q_1 \beta^{n/2} \cdot B + A' = Q_1 \beta^{n/2} \cdot B + Q_0 \cdot B + R = (Q_1 \beta^{n/2} + Q_0) \cdot B + R.$$

よって $Q = Q_1 \beta^{n/2} + Q_0$、余り $R$ が一致。$\square$

再帰内部で発生する $Q_1 \cdot B$ の計算が $n/2 \times n$ 乗算 $O(M(n))$ である。

Master 定理により $D(n) = O(M(n) \log n)$ となる。 $M(n) = O(n^{1.465})$ (Toom-3) や $O(n \log n)$ (FFT) の場合、除算は乗算より $\log n$ 倍遅いだけである。

sangi の実装

// MpnOps.hpp: dcpi1_div_qr_n (Divide-and-Conquer, pre-inverted)
inline uint64_t dcpi1_div_qr_n(uint64_t* qp, uint64_t* np,
                               const uint64_t* dp, size_t n,
                               uint64_t dinv, uint64_t* scratch) {
    if (n < DC_DIV_QR_THRESHOLD) {
        return sbpi1_div_qr(qp, np, 2 * n, dp, n, dinv); // 基底: 学校式
    }
    size_t lo = n / 2, hi = n - lo;
    // 上半分: 2hi-by-hi 再帰
    uint64_t qh = dcpi1_div_qr_n(qp + lo, np + 2 * lo, dp + lo, hi, dinv, scratch);
    // 補正: Q1 × B_low の差し引き、下半分の再帰
    // ...
}

sangi では 2 つの閾値を区別している (値は 2026-05-08 時点):

  • BZ_THRESHOLD = 24: 最上位ディスパッチの切替点。除数が 24 ワード未満なら Knuth D 直接、24 ワード以上なら BZ 再帰に入る。
  • DC_DIV_QR_THRESHOLD = 20: BZ 再帰内部の基底ケース閾値。再帰でサイズが 20 ワード未満まで縮んだら sbpi1_div_qr (学校式) に落とす。

入口閾値 24 と基底閾値 20 の差は小さいが、両者を独立に持つことで「サイズが 24-31 ワードあたりで BZ に入った直後、最初の再帰分割でちょうど 20 ワード台に落ちて基底ケースに切替わる」自然な遷移になる。なお BZ_THRESHOLD は当初 64 だったが、2026-05-08 に sbpi1 (学校式) ループの高速化を反映して 24 に下げられた。

Burnikel-Ziegler の 2n-by-n 除算を 2 つの n-by-n/2 再帰呼出に分解する図 Burnikel-Ziegler 分割統治 $A_3$ $A_2$ $A_1$ $A_0$ 被除数 $A$ (4 ブロック × $n/2$ ワード) $B_1$ $B_0$ 除数 $B$ (2 ブロック × $n/2$ ワード) 再帰 1: $(A_3, A_2, A_1) \div B \to Q_1$ 再帰 2: $(R_1, A_0) \div B \to Q_0$ $Q = Q_1 \beta^{n/2} + Q_0$
図1: Burnikel-Ziegler の 2n-by-n 除算は、上半分の再帰で $Q_1$、続いて部分余り $R_1$ と $A_0$ から下半分の再帰で $Q_0$ を求め、$Q = Q_1 \beta^{n/2} + Q_0$ と結合する。各再帰は半サイズの除算 1 回と $O(M(n))$ の乗算 1 回を含む。

5.4 Newton 逆数反復

除算 $A / B$ を 2 段階に分ける:

  1. $B$ の逆数近似 $X \approx 1/B$ を高速に求める。
  2. $A \cdot X$ で商を計算し、誤差を補正する。

このアプローチでは、除算を 乗算に還元できる。 乗算 $M(n)$ が FFT 等で $O(n \log n)$ なら、除算も同じオーダーになる。 Newton 反復はこの戦略の要である。

Newton 反復式

$f(x) = 1/x - B$ の零点 $x = 1/B$ に対して Newton 法を適用する:

$$x_{k+1} = x_k - \dfrac{f(x_k)}{f'(x_k)} = x_k (2 - B x_k).$$

導出の詳細:反復式と二次収束

Step 1(反復式):$f(x) = 1/x - B$ より $f'(x) = -1/x^2$。Newton 法 $x_{k+1} = x_k - f(x_k)/f'(x_k)$ に代入すると

$$\displaystyle x_{k+1} = x_k - \dfrac{1/x_k - B}{-1/x_k^2} = x_k + (1/x_k - B) x_k^2 = x_k + x_k - B x_k^2 = x_k(2 - B x_k).$$

除算が消える点が肝心($1/B$ を求めるのに $B$ で割らない)。

Step 2(二次収束):誤差を $\varepsilon_k = 1 - B x_k$ とおくと $B x_k = 1 - \varepsilon_k$、よって

$$\displaystyle B x_{k+1} = B x_k (2 - B x_k) = (1 - \varepsilon_k)(2 - (1 - \varepsilon_k)) = (1 - \varepsilon_k)(1 + \varepsilon_k) = 1 - \varepsilon_k^2.$$

つまり $\varepsilon_{k+1} = \varepsilon_k^2$。誤差が毎反復で 2 乗される。$\square$

$x_k$ が $1/B$ に近いとき $B x_k \approx 1$、$2 - B x_k \approx 1$ で、 $x_{k+1}$ の誤差は $x_k$ の誤差の 2 乗に比例する (二次収束)。 精度 $b$ ビットが必要なら、初期近似 $x_0$ (たとえば倍精度浮動小数の 48 ビット) から始めて $\lceil \log_2(b / 48) \rceil$ 回の反復で十分。

各反復のコスト管理

$k$ 回目の反復で $x_k$ の精度が $p_k$ ビットなら、 $B x_k$ の計算では $x_k$ を $p_k$ ビットで保持すればよい。 したがって各反復のコストは $M(p_k)$ で、合計は等比級数:

$$\sum_{k=0}^{K} M(p_k) \le M(n) \cdot \sum_{k=0}^{\infty} 2^{-k} = 2 M(n).$$

つまり、最終精度の乗算 1 回の約 2 倍のコストで逆数が求まる。 これが Newton 反復が「乗算と同じオーダー」で済む秘密である。

sangi の Float 除算での実装

sangi では、Float の除数が 64 ワード (約 4096 ビット) 以上で Newton に切り替わる。 初期近似は倍精度による逆数、反復中は精度を倍々に拡張する。

// Float.cpp: Newton 除算の初期近似
int64_t b_bl = static_cast<int64_t>(b_work.mantissa_.bitLength());
int64_t exp_adj = b_work.exponent_ + b_bl - 1;  // b_norm ∈ [1, 2)
b_work.exponent_ = -(b_bl - 1);
double b_d = b_work.toDouble();                  // 53 bit 近似
Float x(1.0 / b_d);
x.exponent_ -= exp_adj;                          // 元のスケールに戻す
x.effective_bits_ = 64;                          // 達成精度の記録

// Newton 反復本体 (b_work は正規化済みの除数)
int correct_bits = 48;
while (correct_bits < target_bits) {
    Float bx = b_work * x;                       // ≈ 1
    Float residual = one - bx;                   // ≈ 2^{-correct_bits}
    x = x + x * residual;                        // x_{k+1} = x_k (2 − B x_k)
    correct_bits = std::min(2 * correct_bits, target_bits);
}

精度追跡 (effective_bits)

Newton 反復の精度管理は微妙である。$k$ 回目の 目標 精度 $p_k$ と、 実際に達成された 効果的 精度 $\text{effective\_bits}$ は必ずしも一致しない。 反復で用いる丸めの方向、入力誤差の伝播、加減算での桁落ちが原因である。

sangi の実装では、各 Float が effective_bits_ フィールドを持ち、 反復毎に min(2 * correct_bits, step_bits) で更新する。 最終商の計算でも誤差を踏まえた 1 回の補正を行う:

$$Q \approx A \cdot x, \quad \text{residual} = 1 - B \cdot x, \quad Q_{\text{corrected}} = Q + Q \cdot \text{residual}.$$

これは「最終の $M(P, P)$ 乗算を一度節約しつつ誤差を 2 乗で縮める」 fused 補正である。

歴史的補足: 除算アルゴリズムの進化

  • 1962-1973 年: Knuth が The Art of Computer Programming Vol. 2 §4.3.1 で Algorithm D を体系化(初版 1969)。学校式除算の補正条件と正規化の必要性を厳密化。
  • 1971 年: Schönhage-Strassen が FFT 乗算 $O(n \log n \log \log n)$ を提案、除算の Newton 還元と組み合わせて理論上の最適計算量を確立。
  • 1993 年: Jebelean が p-進逆元を用いた Exact Division $O(n)$ を発表。Toom-Cook 補間の小定数除算を高速化。
  • 1998 年: Burnikel-Ziegler が除算専用の分割統治 $O(M(n) \log n)$ を確立し、Karatsuba/Toom と組合せて中サイズ帯を埋める。
  • 2011 年: Möller-Granlund が 3-by-2 商推測でハードウェア除算を実質的に除去、現代 CPU 上の小サイズ性能を 2-3 倍改善。
  • 2019 年: Harvey-van der Hoeven が $O(n \log n)$ 整数乗算を達成、除算も同オーダーに(理論進展、実装は研究段階)。

5.5 Exact Division

「余りが $0$ になることがあらかじめ分かっている」除算を特別扱いできる。 たとえば多項式除算での $A = QB$ の解消、二項係数 $\binom{n}{k} = n! / (k!(n-k)!)$ の整数性、 Toom-Cook 補間で出てくる固定除数 (/3, /5, /11 など) — いずれも余りが $0$ の除算である。

Jebelean の Exact Division

余りを切り捨てない普通の除算では商を上位から求める。 しかし余りが $0$ なら、商を 下位から求めても 結果は同じである。 Jebelean (1993) のアイデアは、$B$ の逆元 $B^{-1} \bmod \beta$ を $p$-進的に使うことである:

$$q_0 \equiv a_0 \cdot B^{-1} \pmod{\beta}, \quad A \leftarrow (A - q_0 B) / \beta.$$

これを $n$ 回繰り返せば商が得られる。各ステップは 1 ワード乗算 + addmul_1 でハードウェア ネイティブ。Möller-Granlund の逆数推測も不要。結果として:

$$T_{\text{exact}}(n) = O(n) \cdot T_{\text{mul\_1}} \approx 0.3 \times T_{\text{knuth\_D}}(n).$$

用途

  • Toom-Cook 補間: 評価値を Vandermonde 逆行列で戻すとき、固定の小整数で割る (第8章 参照)。 Toom-8 では /11, /17, /31 といった小素数除算が 10〜20 回入る。
  • 二項係数: $\binom{n}{k}$ を逐次更新 $\binom{n}{k} = \binom{n}{k-1} \cdot (n-k+1) / k$ で 求めると、各ステップは exact division。
  • Binary Splitting: 有理級数の結合で共通因子の除去 (第10章 参照)。

divexact_by_odd の威力

sangi では Toom-8 実装のなかで divexact_by_odd(c, 3) のような固定除数 exact division を多用する。奇数 $d$ の $\beta$ での逆元 $d^{-1} \bmod \beta$ は 定数として埋め込めるため、全体が addmul_1 ループに折り畳める。 2026-04-19 の最適化で、この折り畳みにより 524K 除算が $11\%$ 改善した (684 us → 609 us)。

なお divexact_by_odd の名の通り、この最適化は除数が奇数であることが前提である。 偶数除数を扱う場合は事前に 2 の冪を右シフトで取り除き、残る奇数部分にのみ適用する。

5.6 特殊定数の除算

2 の冪での除算

$A / 2^k$ は単なる右シフトである。 ワード境界に揃っていれば ($k$ が 64 の倍数) ポインタの付け替えだけで済む。 揃っていなくても各ワードを SHRD 命令で 2 ワードまたぎにシフトすればよい。 コストは $O(n)$ の線形時間だが、乗除算と違って分岐も補正もない。

10 の冪での除算

10 進出力や有効桁管理では $A / 10^k$ が頻繁に必要になる。 $10 = 2 \cdot 5$ なので、$A / 10^k = (A / 2^k) / 5^k$。右シフト後に 固定除数 $5^k$ で exact division する。

小定数での除算

$d \in \{3, 5, 7, \ldots\}$ のような小定数での除算は、 Möller-Granlund の divide by invariant integer 法で実装する:

$$\hat{q} = \lfloor a \cdot \mu / 2^{64+\ell} \rfloor, \quad \mu = \lfloor 2^{64+\ell} / d \rfloor.$$

$\mu$ は事前計算できるので、実行時は 1 回の MULH (上位半分の乗算) とシフト。 最大で 1 回の補正が必要だが、それを含めても 1 ワード除算の数倍速い。

5.7 アルゴリズム選択の閾値 — sangi の設計

高速アルゴリズムには、基本的に「定数項が大きいが漸近的に優位」という性質がある。 小さい入力では素朴なアルゴリズムの方が速いため、サイズごとにディスパッチする必要がある。 sangi の除算ディスパッチは次の通り。

sangi の除算閾値 (2026-05 時点)

除数サイズ $b_n$ (ワード) アルゴリズム 計算量
$b_n < 24$ Knuth D + Möller-Granlund 3-by-2 (div_basecase) $O(n \cdot b_n)$
$24 \le b_n < 5000$ (balanced) Burnikel-Ziegler (dcpi1_div_qr_n) $O(M(n) \log n)$
$b_n \ge 5000$ (balanced) $\mu$-div (逆数近似 + Barrett)、MU_DIV_BALANCED_THRESHOLD $O(M(n))$
不均衡 $k = n/b_n$ に応じた 3 段階 $\mu$-div: $k \ge 8$ で $b_n \ge 150$、$k=4{-}7$ で $b_n \ge 500$、$k=3$ で $b_n \ge 2500$ $O(M(n))$
Float, $b_n \ge 64$ Newton 逆数反復 (NEWTON_DIV_THRESHOLD = 64) $O(M(n))$
Exact div (固定小除数) Jebelean / divexact_by_odd $O(n)$

なぜ Int では $\mu$-div を使い Float では Newton を使うのか — Int の商は整数のため 余り $R$ を同時に計算しなければならず、Barrett 還元ベースの $\mu$-div が向いている。 一方 Float は商だけを必要とし、精度も離散的でなく連続的に管理できるため Newton が自然である。

閾値の調整は経験的

「いつ BZ に切り替えるか」の正確な値は CPU のキャッシュサイズ、 乗算ルーチンの定数、コンパイラ最適化に依存する。 sangi の 24 ワードは Zen 3 + GCC/MSVC Release での測定値であり、 別の CPU では別の値が最適になる。 小さすぎると再帰のオーバーヘッドが学校式の $O(n^2)$ 優位を食い潰し、 大きすぎると漸近的に劣る $O(n^2)$ がいつまでも効いて遅くなる。 実用ライブラリではプラットフォーム依存の閾値を自動チューニングで決定するのが一般的である。

他ライブラリの参考

多倍長整数除算の代表的実装として GMP がデファクト標準であり、 Knuth D + Möller-Granlund 3-by-2 / Burnikel-Ziegler / Newton 還元の組合せを基本とする。 本章で扱った主要アルゴリズムはどれも GMP に採用されており、 sangi もこの系譜に属する。sangi の差別化点は乗算層に 5-smooth NTT(サイズ $2^a 3^b 5^c$)と Toom-8 までの多段戦略を採用している点である(第9章 参照)。

5.8 実測性能

sangi の除算を代表的なサイズで実測し、サイズ帯別の特性を観察する。 測定環境は Zen 3 (Ryzen 5 5600X), MSVC Release x64, single thread。

除算性能 (バランス除算, $n / n$)

サイズ sangi (μs) 主に担当する乗算層
$\le 65$ K bit < 100 学校式 + BZ + Karatsuba
262 K bit 598 BZ + Toom-4 / Toom-6
524 K bit 1834 Newton + Toom-8 + NTT
1 M bit 3850 Newton + 5-smooth NTT

値は 2026-04-19 の Toom-8 最適化 + divexact_by_odd 折り畳み後の実測。

サイズ帯別の実装特性

除算の速度は 除算そのものではなく、内部で呼ばれる 乗算の速度で大部分が決まる。 Newton ベースの $\mu$-div では逆数計算に不均衡乗算 (被乗数 $\gg$ 乗数) が多数発生する。

  • NTT 域 ($n \ge 1200$ ワード): 5-smooth NTT が $O(n \log n)$ の漸近優位を発揮する領域。
  • Karatsuba 域 ($22 \le n < 140$ ワード): 古典的 Karatsuba による $O(n^{1.585})$ が最速。
  • Toom-6/Toom-8 域 (600-1200 ワード): 高次 Toom-Cook が最速。Toom-8 の pre-pad 評価が効く。
  • invert_approx の 2:1 不均衡乗算 (513-1025 ワード): Toom-4,2 特化で不均衡のピースサイズを揃える。

つまり「除算を速くする」には「除算の内側の乗算を速くする」のが本筋である。 524K bit の除算が大きく短縮された要因も、 5-smooth NTT (第9章) の導入と Toom-8 の最適化 (2026-04-19) にあった。

計測上の注意

ベンチマークは CPU の熱状態、メモリ帯域、コンパイラ版に敏感である。 連続実行でサーマルスロットリングが起きると絶対時間が数 $\%$ 悪化する。 sangi のベンチスイートは各サイズの前後に 30 秒の cooldown を入れ、 同じマシン・同じセッションで繰り返し測定することで 絶対値のドリフトを打ち消している。

5.9 まとめ

  • Knuth D と 3-by-2 推測: 小サイズでは学校式 + Möller-Granlund の事前計算逆数が最速。 推測誤差が $\{0, 1\}$ に収まるよう正規化が前提。
  • Burnikel-Ziegler: 分割統治で $O(M(n) \log n)$ を達成。 sangi は 24 ワード (BZ_THRESHOLD) から BZ に切り替える。
  • Newton 逆数反復: 除算を乗算に還元、$O(M(n))$ 達成。 二次収束なので倍精度初期値から数回で最終精度に到達。 精度追跡 (effective_bits) を誤ると商が壊れる。
  • Exact Division: 余り 0 が保証される場合は Jebelean の $p$-進版で $O(n)$。 Toom-Cook 補間の小定数除算に多用される。
  • 性能の主因は乗算: 除算の速度は大部分「内部で呼ばれる乗算」の速度で決まる。 sangi が 524K bit で達成した改善も、NTT と Toom-Cook の最適化に由来する。

次章では、除算の特殊形であるモジュラー算術 ($a \bmod m$) を扱う。 RSA や NTT のホットループで現れる Barrett 還元・Montgomery 乗算は、 本章の除算を「除算しない」方向に再構成する技術である。

参考文献

  • Knuth, D.E. The Art of Computer Programming, Vol. 2: Seminumerical Algorithms, 3rd ed., Addison-Wesley, 1997. §4.3.1 (Algorithm D)。
  • Burnikel, C., Ziegler, J. "Fast Recursive Division", MPI-I-98-1-022, Max-Planck-Institut für Informatik, 1998.
  • Möller, N., Granlund, T. "Improved Division by Invariant Integers", IEEE Trans. Computers, 60(2), pp. 165-175, 2011.
  • Brent, R.P., Zimmermann, P. Modern Computer Arithmetic, Cambridge University Press, 2010. 第1章。
  • Jebelean, T. "An Algorithm for Exact Division", J. Symbolic Computation, 15(2), pp. 169-180, 1993.
  • Hasselström, K. "Fast Division of Large Integers — A Comparison of Algorithms", Master's thesis, KTH, 2003.

関連章

よくある質問

高速除算はなぜ乗算に帰着されるのか?

除算は逆数の近似値を Newton 法で求め、それを乗算することで実現できる。Newton 法は2次収束するため、乗算の $O(n \log n)$ と同じオーダーで除算が計算できる。

Barrett還元と Montgomery還元の違いは何か?

Barrett還元は事前に法の逆数近似を計算してシフト・乗算で除算を避ける手法、Montgomery還元は $2^k$ を法として演算することでモジュラ除算をシフトに置き換える手法である。いずれも繰り返しモジュラ演算を高速化する。

Exact Division(割り切れる場合の除算)の特別な最適化とは?

除数で割り切れることが事前にわかっている場合、下位桁から逆元計算で1桁ずつ商を決定できる。桁上がり不要で演算回数を半減でき、GCD 計算や除数が既知の場合に有効である。