第4章: 高速乗算アルゴリズム

Fast Multiplication Algorithms

前章では、多倍長整数の筆算乗算が $O(n^2)$ であることを見た。 本章では、この計算量を大幅に改善する高速乗算アルゴリズムを詳しく学ぶ。 Karatsuba法($O(n^{1.585})$)、Toom-Cook法($O(n^{1.465})$)、 そしてFFT乗算($O(n \log n \log \log n)$)を、 数学的な証明・具体的な数値例・擬似コードとともに解説する。

乗算は多倍長計算の中核的な演算であり、 除算・平方根・初等関数の計算もすべて乗算に帰着される。 したがって、乗算の高速化はすべての多倍長演算の性能を決定する鍵となる。

1. Karatsuba法

1962年、Anatolii Karatsuba は、$n$ 桁の乗算に必ず $O(n^2)$ かかるという Kolmogorov の予想を覆す画期的なアルゴリズムを発見した。 そのアイデアは単純かつ美しい:$n$ 桁の乗算を 4 回の $n/2$ 桁乗算ではなく 3 回の $n/2$ 桁乗算に帰着させるのである。

1.1 原理

Karatsuba法の原理

$n$ リムの整数 $A$, $C$ を上位・下位に分割する($m = \lceil n/2 \rceil$):

$$A = A_1 \cdot B^{m} + A_0, \quad C = C_1 \cdot B^{m} + C_0$$

ここで $B$ は基数(例:$B = 2^{32}$ や $B = 2^{64}$)、$A_0, A_1, C_0, C_1$ は $m$ リム以下の整数。$A_0, C_0$ は下位 $m$ リム、$A_1, C_1$ は上位リムである。

1.2 定理と証明

定理(Karatsuba法の正当性)

$A \times C$ は以下の3回の乗算で計算できる:

$$A \times C = z_2 \cdot B^{2m} + z_1 \cdot B^{m} + z_0$$

ここで:

  • $z_0 = A_0 \times C_0$(下位同士の積)
  • $z_2 = A_1 \times C_1$(上位同士の積)
  • $z_1 = (A_0 + A_1)(C_0 + C_1) - z_0 - z_2$(交差項)

証明

Step 1:直接展開

$A = A_1 B^m + A_0$, $C = C_1 B^m + C_0$ を直接掛け合わせると:

$$\begin{align} A \times C &= (A_1 B^m + A_0)(C_1 B^m + C_0) \\ &= A_1 C_1 \cdot B^{2m} + (A_1 C_0 + A_0 C_1) \cdot B^m + A_0 C_0 \end{align}$$

この式には $A_1 C_1$, $A_1 C_0$, $A_0 C_1$, $A_0 C_0$ の4回の乗算が含まれる。

Step 2:中央項の変形(Karatsubaのトリック)

$(A_0 + A_1)(C_0 + C_1)$ を展開すると:

$$(A_0 + A_1)(C_0 + C_1) = A_0 C_0 + A_0 C_1 + A_1 C_0 + A_1 C_1$$

したがって、交差項は既に計算済みの $z_0, z_2$ を用いて表せる:

$$A_1 C_0 + A_0 C_1 = (A_0 + A_1)(C_0 + C_1) - A_0 C_0 - A_1 C_1 = z_1$$

Step 3:結論

$z_0 = A_0 C_0$, $z_2 = A_1 C_1$, $z_1 = (A_0 + A_1)(C_0 + C_1) - z_0 - z_2$ とおくと:

$$A \times C = z_2 \cdot B^{2m} + z_1 \cdot B^m + z_0$$

必要な$n/2$ リムの乗算は $z_0$, $z_2$, $(A_0 + A_1)(C_0 + C_1)$ の3回のみであり、 残りは加減算($O(n)$)とシフト($O(n)$)で済む。$\square$

筆算との比較:なぜ1回減るのか

筆算では $A_0 C_0$, $A_0 C_1$, $A_1 C_0$, $A_1 C_1$ の4回の乗算が必要であった。Karatsuba法は、$A_0 C_1$ と $A_1 C_0$ を個別に求める代わりに、その $A_0 C_1 + A_1 C_0$ を1回の乗算で求めることにより、乗算回数を4から3に削減する。

代償として加減算の回数が増えるが、加減算は $O(n)$ であり乗算 $O(n^2)$ より遥かに軽いため、全体として高速化が得られる。

1.3 計算量の解析

定理(Karatsuba法の計算量)

$n$ リムの乗算にかかる時間を $T(n)$ とすると、Karatsuba法では:

$$T(n) = 3T(\lceil n/2 \rceil) + O(n)$$

したがって $T(n) = O(n^{\log_2 3}) = O(n^{1.585\ldots})$ である。

計算量の証明

マスター定理(Master Theorem)を適用する。漸化式 $T(n) = aT(n/b) + f(n)$ において:

  • $a = 3$(部分問題の数=乗算回数)
  • $b = 2$(問題サイズの縮小率)
  • $f(n) = O(n)$(加減算・シフトのコスト)

比較指数は $\log_b a = \log_2 3 \approx 1.585$ である。$f(n) = O(n) = O(n^{1.585 - 0.585})$ であるから、$f(n) = O(n^{\log_b a - \epsilon})$($\epsilon = 0.585 > 0$)が成り立つ。

マスター定理の Case 1 より:

$$T(n) = \Theta(n^{\log_2 3}) \approx O(n^{1.585})$$

筆算の $O(n^2)$ と比較すると、例えば $n = 1000$ リムの場合:

  • 筆算:$1000^2 = 10^6$ 回の乗算
  • Karatsuba:$1000^{1.585} \approx 47800$ 回の乗算(約21倍高速)

$n$ が大きいほど差は開く。$\square$

1.4 擬似コード

Karatsuba法の擬似コード

function karatsuba(A[0..n-1], C[0..n-1]):
    // 基底ケース:小さな整数は筆算で計算
    if n <= THRESHOLD:
        return schoolbook_multiply(A, C)

    // 分割
    m = ceil(n / 2)
    A0 = A[0..m-1]         // 下位 m リム
    A1 = A[m..n-1]         // 上位リム
    C0 = C[0..m-1]         // 下位 m リム
    C1 = C[m..n-1]         // 上位リム

    // 3 回の再帰的乗算
    z0 = karatsuba(A0, C0)              // A0 * C0
    z2 = karatsuba(A1, C1)              // A1 * C1
    z1 = karatsuba(A0 + A1, C0 + C1)   // (A0+A1)(C0+C1)
    z1 = z1 - z0 - z2                  // 交差項

    // 結合(シフトと加算)
    result = z0
    result += z1 << (m * BITS_PER_LIMB)    // z1 * B^m
    result += z2 << (2*m * BITS_PER_LIMB)  // z2 * B^(2m)
    return result

THRESHOLD は筆算に切り替える閾値で、 典型的には 30〜80 リム程度に設定される。 この値はアーキテクチャやキャッシュサイズに依存し、 ベンチマークにより最適値を決定する。

実装上の注意点

  • $A_0 + A_1$ の桁あふれ: $A_0$ と $A_1$ はそれぞれ $m$ リム以下であるが、その和 $A_0 + A_1$ は $m + 1$ リムになりうる。この追加の 1 リムを適切に処理する必要がある。
  • $z_1$ の符号: $z_1 = (A_0 + A_1)(C_0 + C_1) - z_0 - z_2$ は常に非負であるとは限らない。 実際、$A_0 C_1 + A_1 C_0 \geq 0$ は常に成り立つが、 中間計算で一時的に大きな値を扱う必要がある。
  • 異なるサイズの乗算: $n$ リム $\times$ $m$ リム($n \neq m$)の場合、短い方をゼロパディングするか、 分割を非対称に行う工夫が必要である。
  • メモリ管理: 再帰呼び出しごとに一時配列を確保すると、メモリ使用量が増大する。 GMP では一時メモリをスタック的に管理し、不要なメモリ割り当てを回避している。

1.5 具体的な数値例

例1:$1234 \times 5678$ を Karatsuba法で計算($B = 100$)

基数 $B = 100$ を用いる(2桁ずつ区切る)。

$A = 1234 = 12 \times 100 + 34$、$C = 5678 = 56 \times 100 + 78$

  • $A_1 = 12$, $A_0 = 34$
  • $C_1 = 56$, $C_0 = 78$

Step 1:3回の乗算を実行する

  • $z_0 = A_0 \times C_0 = 34 \times 78 = 2652$
  • $z_2 = A_1 \times C_1 = 12 \times 56 = 672$
  • $(A_0 + A_1)(C_0 + C_1) = (34 + 12)(78 + 56) = 46 \times 134 = 6164$

Step 2:交差項を計算する

$z_1 = 6164 - 2652 - 672 = 2840$

Step 3:結合する

$$\begin{align} A \times C &= z_2 \times B^2 + z_1 \times B + z_0 \\ &= 672 \times 10000 + 2840 \times 100 + 2652 \\ &= 6720000 + 284000 + 2652 \\ &= 7006652 \end{align}$$

検算:$1234 \times 5678 = 7006652$ ✓

例2:再帰的な適用($B = 10$)

$A = 1234$, $C = 5678$ を $B = 10$ で Karatsuba 法を2段階再帰的に適用する。

第1段階の分割($m = 2$, $B^m = 100$):

  • $A = 12 \times 100 + 34$, $C = 56 \times 100 + 78$

$z_0 = 34 \times 78$ にさらに Karatsuba を適用($m = 1$, $B^m = 10$):

  • $34 = 3 \times 10 + 4$, $78 = 7 \times 10 + 8$
  • $z_0' = 4 \times 8 = 32$
  • $z_2' = 3 \times 7 = 21$
  • $(4+3)(8+7) = 7 \times 15 = 105$
  • $z_1' = 105 - 32 - 21 = 52$
  • $34 \times 78 = 21 \times 100 + 52 \times 10 + 32 = 2100 + 520 + 32 = 2652$ ✓

同様に $z_2 = 12 \times 56 = 672$、$(A_0+A_1)(C_0+C_1) = 46 \times 134 = 6164$ も再帰的に計算される。

Karatsuba法の可視化

Karatsuba法:1234 × 5678(B = 100) 分割: 12 34 = 1234 A₁ A₀ 56 78 = 5678 C₁ C₀ 3回の乗算: z₀ = A₀ × C₀ 34 × 78 = 2652 z₂ = A₁ × C₁ 12 × 56 = 672 (A₀+A₁)(C₀+C₁) 46 × 134 = 6164 交差項: z₁ = 6164 − 2652 − 672 = 2840 結合: z₂ × B² 672 × 10000 = 6720000 z₁ × B 2840 × 100 = 284000 z₀ 2652 = 2652 合計 = 7006652 1234 × 5678 = 7006652 乗算3回 + 加減算で完了 ✓
図 1: Karatsuba 法の処理フロー — 桁分割で 3 個の再帰乗算に圧縮し合成

2. Toom-Cook法

Toom-Cook法は Karatsuba法の自然な一般化であり、 整数を多項式として表現し、多項式補間の技法を用いて乗算を行う。 Karatsuba法が 2分割・3点評価であるのに対し、 Toom-$k$ 法は $k$ 分割・$(2k-1)$ 点評価を用いる。

2.1 Toom-3法の原理

Toom-3法(Toom-Cook-3)

$n$ リムの整数 $A$, $C$ を3つに分割する($m = \lceil n/3 \rceil$):

$$A = A_2 \cdot R^{2} + A_1 \cdot R + A_0, \quad C = C_2 \cdot R^{2} + C_1 \cdot R + C_0$$

ここで $R = B^m$($B$ は基数)。これを多項式とみなす:

$$p(x) = A_2 x^2 + A_1 x + A_0, \quad q(x) = C_2 x^2 + C_1 x + C_0$$

すると $A = p(R)$, $C = q(R)$ であり、$A \times C = p(R) \cdot q(R) = r(R)$

2.2 正当性の証明

定理(Toom-3法の正当性)

積の多項式 $r(x) = p(x) \cdot q(x)$ は高々4次なので、5つの係数を持つ:

$$r(x) = r_4 x^4 + r_3 x^3 + r_2 x^2 + r_1 x + r_0$$

この5つの係数は、5点での評価値から5回の乗算で決定できる。

証明

Step 1:評価(Evaluation)

5つの点 $x = 0, 1, -1, 2, \infty$ で $p(x)$ と $q(x)$ を評価する:

$x$ $p(x)$ $q(x)$ 計算コスト
$0$ $A_0$ $C_0$ なし
$1$ $A_2 + A_1 + A_0$ $C_2 + C_1 + C_0$ 加算 $\times 2$
$-1$ $A_2 - A_1 + A_0$ $C_2 - C_1 + C_0$ 加減算 $\times 2$
$2$ $4A_2 + 2A_1 + A_0$ $4C_2 + 2C_1 + C_0$ シフト+加算
$\infty$ $A_2$(最高次係数) $C_2$(最高次係数) なし

各評価値は加減算とシフトのみで計算可能($O(n)$)。

導出の詳細:$r(\infty)$ とは何か

形式的には次の極限で定義する:

$$\displaystyle r(\infty) := \lim_{x \to \infty} \dfrac{r(x)}{x^{\deg r}} = \lim_{x \to \infty} \dfrac{r_0 + r_1 x + \cdots + r_4 x^4}{x^4} = r_4.$$

同様に $p(\infty) = A_2$、$q(\infty) = C_2$。$r(\infty) = p(\infty) \cdot q(\infty) = A_2 C_2$ は最上位係数同士の積であり、追加の乗算を要しない(既に確保される情報)。これが評価点として「無限大」を採用する利点である。

Step 2:点ごとの乗算(Pointwise Multiplication)

各点で $r(x_i) = p(x_i) \cdot q(x_i)$ を計算する。これが5回の乗算(再帰的に実行)である:

  • $r(0) = p(0) \cdot q(0) = A_0 C_0$
  • $r(1) = p(1) \cdot q(1) = (A_0 + A_1 + A_2)(C_0 + C_1 + C_2)$
  • $r(-1) = p(-1) \cdot q(-1) = (A_0 - A_1 + A_2)(C_0 - C_1 + C_2)$
  • $r(2) = p(2) \cdot q(2) = (A_0 + 2A_1 + 4A_2)(C_0 + 2C_1 + 4C_2)$
  • $r(\infty) = A_2 C_2$

Step 3:補間(Interpolation)

5つの評価値 $r(0), r(1), r(-1), r(2), r(\infty)$ から係数 $r_0, r_1, r_2, r_3, r_4$ を復元する。これはVandermonde行列の逆行列を適用することに相当する:

$$\begin{pmatrix} r(0) \\ r(1) \\ r(-1) \\ r(2) \\ r(\infty) \end{pmatrix} = \begin{pmatrix} 1 & 0 & 0 & 0 & 0 \\ 1 & 1 & 1 & 1 & 1 \\ 1 & -1 & 1 & -1 & 1 \\ 1 & 2 & 4 & 8 & 16 \\ 0 & 0 & 0 & 0 & 1 \end{pmatrix} \begin{pmatrix} r_0 \\ r_1 \\ r_2 \\ r_3 \\ r_4 \end{pmatrix}$$

逆行列は定数(入力サイズに依存しない)なので、係数の復元は加減算と小さな定数での除算のみで済む($O(n)$)。

Step 4:再構成(Recomposition)

$$A \times C = r(R) = r_4 R^4 + r_3 R^3 + r_2 R^2 + r_1 R + r_0$$

$R = B^m$ なので、これはシフトと加算で実行できる($O(n)$)。$\square$

2.3 補間の具体的な公式

Toom-3の補間公式

評価値から係数を復元する閉じた公式は以下の通りである:

$$\begin{align} r_0 &= r(0) \\ r_4 &= r(\infty) \\ t_1 &= \dfrac{r(1) - r(-1)}{2} \quad \text{(奇数部分)} \\ t_2 &= \dfrac{r(1) + r(-1)}{2} - r(0) \quad \text{(偶数部分} - r_0 \text{)} \\ r_3 &= \dfrac{r(2) - r(0) - 4t_2 - 2t_1}{6} - r_4 \\ r_1 &= t_1 - r_3 \\ r_2 &= t_2 - r_4 \end{align}$$

$t_1$ と $t_2$ は中間変数である。重要なのは、2 での除算はビットシフト、6 での除算は $2$ と $3$ での除算に分解できるという点である。$3$ での除算は事前計算した逆数を用いた乗算で実現可能である。

導出の詳細:補間公式はどこから来るか

$r(x) = r_0 + r_1 x + r_2 x^2 + r_3 x^3 + r_4 x^4$ に評価点を代入して連立方程式を得る:

$$\displaystyle r(0) = r_0, \quad r(\infty) = r_4, \quad r(1) = r_0+r_1+r_2+r_3+r_4, \quad r(-1) = r_0-r_1+r_2-r_3+r_4.$$

Step 1(偶奇分離):$r(1) \pm r(-1)$ で偶数次項と奇数次項を分離する。

$$\dfrac{r(1) - r(-1)}{2} = r_1 + r_3 \;(\equiv t_1), \quad \dfrac{r(1) + r(-1)}{2} - r_0 = r_2 + r_4 \;(\equiv t_2).$$

Step 2($r_3$ の抽出):$r(2) = r_0 + 2r_1 + 4r_2 + 8r_3 + 16r_4$ から $r_0$、$4t_2 = 4r_2 + 4r_4$、$2t_1 = 2r_1 + 2r_3$ を引くと $6r_3 + 12r_4$ が残る。これを 6 で割って $r_4$ を引けば $r_3$ が得られる。

Step 3:$r_1 = t_1 - r_3$、$r_2 = t_2 - r_4$ で残りを復元。これがちょうど Vandermonde 逆行列を適用した結果になっている。$\square$

2.4 計算量の解析

定理(Toom-3法の計算量)

漸化式 $T(n) = 5T(n/3) + O(n)$ より、マスター定理を適用すると:

$$T(n) = \Theta(n^{\log_3 5}) \approx O(n^{1.465})$$

Karatsuba法の $O(n^{1.585})$ より漸近的に高速である。

2.5 一般のToom-$k$ 法

一般のToom-$k$ 法の比較

$k$ 分割すると、積の多項式は高々 $2(k-1)$ 次になるので、$2k-1$ 点での評価が必要である。

方法 分割数 $k$ 乗算回数 $2k-1$ 計算量 指数
Karatsuba (Toom-2) 2 3 $O(n^{\log_2 3})$ $\approx 1.585$
Toom-3 3 5 $O(n^{\log_3 5})$ $\approx 1.465$
Toom-4 4 7 $O(n^{\log_4 7})$ $\approx 1.404$
Toom-$k$ $k$ $2k-1$ $O(n^{\log_k(2k-1)})$ $\to 1$

$k \to \infty$ で指数は $1$ に近づくが、補間ステップの定数項(加減算・小整数除算の回数)が $O(k^2)$ で増大するため、実用上は Toom-3 か Toom-4 までが使われる。それ以上の高速化にはFFTベースの方法が有利である。

なぜ $2k-1$ 回の乗算で済むのか

次数 $k-1$ の多項式同士の積は次数 $2(k-1) = 2k-2$ の多項式になる。$2k-2$ 次多項式は $2k-1$ 個の係数を持ち、ラグランジュ補間の定理により $2k-1$ 点での値で一意に決まる。したがって、$2k-1$ 点で評価値を計算すれば($2k-1$ 回の $n/k$ リム乗算)、係数を復元できる。$\square$

3. FFT乗算

Toom-Cook法では分割数 $k$ を増やすほど漸近的な指数が $1$ に近づくが、 補間のオーバーヘッドが増大する。FFT(高速フーリエ変換)を用いると、 本質的に $O(n \log n)$ に近い計算量で乗算を実現できる。

3.1 乗算と畳み込み

多項式乗算としての整数乗算

$n$ リムの整数 $A = \displaystyle\sum_{i=0}^{n-1} a_i B^i$ は、形式的に多項式 $A(x) = \displaystyle\sum_{i=0}^{n-1} a_i x^i$ と同一視できる。2つの整数の積 $P = A \times C$ は、多項式の積 $P(x) = A(x) \cdot C(x)$ を $x = B$ で評価したもの(に繰り上がり処理を加えたもの)に等しい。

多項式の積の係数は畳み込み(convolution)で表される:

$$p_k = \displaystyle\sum_{i+j=k} a_i c_j = \displaystyle\sum_{i=0}^{k} a_i c_{k-i}$$

直接計算すると $O(n^2)$ であるが、FFTと畳み込み定理を用いると $O(n \log n)$ で計算できる。

3.2 畳み込み定理

畳み込み定理(Convolution Theorem)

長さ $N$ の系列 $\{a_i\}$ と $\{c_i\}$ の畳み込み $\{p_k\}$ について:

$$\mathcal{F}(a * c) = \mathcal{F}(a) \cdot \mathcal{F}(c)$$

ここで $\mathcal{F}$ は離散フーリエ変換(DFT)、右辺の $\cdot$ は各成分ごとの積(pointwise multiplication)である。したがって:

$$a * c = \mathcal{F}^{-1}\bigl(\mathcal{F}(a) \cdot \mathcal{F}(c)\bigr)$$

DFT は FFT(Cooley-Tukey アルゴリズム)により $O(N \log N)$ で計算できるため、畳み込み全体が $O(N \log N)$ で求まる。

3.3 FFT乗算のアルゴリズム

FFT乗算の手順

  1. ゼロパディング:$A$ と $C$ の係数列をそれぞれ長さ $N \geq 2n$(2の冪乗に切り上げ)に拡張し、高位の係数を 0 で埋める。巡回畳み込みではなく線形畳み込みを得るために必要。
  2. 順変換:$\hat{A} = \text{FFT}(A)$, $\hat{C} = \text{FFT}(C)$ を計算する(各 $O(N \log N)$)。
  3. 成分ごとの積:$\hat{P}_k = \hat{A}_k \cdot \hat{C}_k$($k = 0, 1, \ldots, N-1$)を計算する($O(N)$)。
  4. 逆変換:$P = \text{IFFT}(\hat{P})$ で畳み込み結果を得る($O(N \log N)$)。
  5. 繰り上がり処理:$p_k \geq B$ の場合、$p_k \bmod B$ を残して $\lfloor p_k / B \rfloor$ を上位に加算する($O(N)$)。

例:FFT乗算の概念的な流れ($A = (3, 2, 1)$, $C = (6, 5, 4)$, $B = 10$)

$A = 123$, $C = 456$ を基数 $B = 10$ で乗算する。

Step 1:ゼロパディング(長さ $N = 8$ に拡張)

  • $a = (3, 2, 1, 0, 0, 0, 0, 0)$
  • $c = (6, 5, 4, 0, 0, 0, 0, 0)$

Step 2-3:FFT → 成分ごとの積 → IFFT

(畳み込みの結果として以下の係数列が得られる)

  • $p = (18, 27, 28, 13, 4, 0, 0, 0)$

検算:$p_0 = 3 \times 6 = 18$、$p_1 = 3 \times 5 + 2 \times 6 = 27$、$p_2 = 3 \times 4 + 2 \times 5 + 1 \times 6 = 28$、$p_3 = 2 \times 4 + 1 \times 5 = 13$、$p_4 = 1 \times 4 = 4$。

Step 4:繰り上がり処理($B = 10$)

  • $p_0 = 18$:$8$ を残し、$1$ を繰り上げ → $(8, 28, 28, 13, 4)$
  • $p_1 = 28$:$8$ を残し、$2$ を繰り上げ → $(8, 8, 30, 13, 4)$
  • $p_2 = 30$:$0$ を残し、$3$ を繰り上げ → $(8, 8, 0, 16, 4)$
  • $p_3 = 16$:$6$ を残し、$1$ を繰り上げ → $(8, 8, 0, 6, 5)$

結果:$56088$(上位から $5, 6, 0, 8, 8$)。$123 \times 456 = 56088$ ✓

3.4 浮動小数点FFTの問題と対策

丸め誤差の問題

複素数を用いた標準的なFFTでは、$\cos$ と $\sin$ の計算やフーリエ変換の各バタフライ演算で浮動小数点の丸め誤差が蓄積する。$N$ 点FFTでは約 $O(\log N)$ ビットの誤差が生じるため、正しい結果を保証するには以下の対策が必要である:

  • リムの分割:64ビットリムをそのまま使うと中間値が巨大になるため、各リムを 16〜20 ビット程度の小片に分割してからFFTを適用する。これにより必要な精度が下がり、倍精度浮動小数点で十分になる場合がある。
  • 多倍長浮動小数点FFT:MPFR などの多倍長浮動小数点を用いてFFTを行う。丸め誤差を完全に制御できるが、定数倍のオーバーヘッドがかかる。
  • 整数FFT(NTT):丸め誤差を完全に排除する最も根本的な対策。次節で詳述する。

3.5 NTT(数論変換)

NTT(Number Theoretic Transform, 数論変換)

複素数体 $\mathbb{C}$ の代わりに有限体 $\mathbb{Z}/p\mathbb{Z}$ 上でFFTを行う手法である。$p$ を素数、$g$ を $\mathbb{Z}/p\mathbb{Z}$ の原始根とすると、

$$\omega = g^{(p-1)/N} \bmod p$$

は位数 $N$ の元であり、$\omega^N \equiv 1 \pmod{p}$、$\omega^k \not\equiv 1 \pmod{p}$($0 < k < N$)を満たす。この $\omega$ が FFT における $N$ 乗根 $e^{2\pi i/N}$ の役割を果たす。

導出の詳細:なぜ $\omega$ は位数 $N$ になるか

$N$ が $p-1$ を割り切ることを前提とする(NTT-friendly prime の必要条件)。

Step 1:$g$ は原始根なので $g$ の位数は $p-1$、すなわち $g^{p-1} \equiv 1$ かつ $g^k \not\equiv 1$($0 < k < p-1$)。

Step 2:$\omega^N = (g^{(p-1)/N})^N = g^{p-1} \equiv 1 \pmod{p}$。

Step 3:$0 < k < N$ で $\omega^k \equiv 1$ と仮定すると $g^{k(p-1)/N} \equiv 1$。$g$ の位数 $p-1$ より $(p-1) \mid k(p-1)/N$、すなわち $N \mid k$、これは $k < N$ に矛盾。$\square$

FFT で必要な性質「$\omega^{N/2} = -1$」も同様に:$\omega^{N/2} = g^{(p-1)/2}$ で、原始根の指数定理から $g^{(p-1)/2} \equiv -1 \pmod{p}$ となる($N$ が偶数のとき)。

すべての演算は整数の剰余演算で行われるため、丸め誤差は一切発生しない

例:NTTによる多項式乗算($p = 17$, $N = 4$)

$p = 17$ を法とし、$N = 4$ 点の NTT を実行する。$p - 1 = 16 = 4 \times 4$ より $N = 4$ は $p - 1$ を割り切る。

まず $\omega$ を求める。$g = 3$ は $17$ の原始根である($3^{16} \equiv 1 \pmod{17}$, $3^8 \equiv 16 \not\equiv 1$)。

$$\omega = 3^{(17-1)/4} = 3^4 = 81 \equiv 81 - 4 \times 17 = 81 - 68 = 13 \pmod{17}$$

検算:$\omega^4 = 13^4 = 28561 = 1680 \times 17 + 1$。したがって $\omega^4 \equiv 1 \pmod{17}$ ✓

$\omega^2 = 13^2 = 169 = 9 \times 17 + 16 \equiv 16 \equiv -1 \pmod{17}$ ✓(位数4であることの確認)

$A(x) = 2x + 3$, $C(x) = 4x + 1$ の積を NTT で計算する。

$a = (3, 2, 0, 0)$, $c = (1, 4, 0, 0)$(ゼロパディング済み)

NTT(順変換):$\hat{a}_k = \displaystyle\sum_{j=0}^{3} a_j \omega^{jk} \pmod{17}$

  • $\hat{a}_0 = 3 + 2 + 0 + 0 = 5$
  • $\hat{a}_1 = 3 + 2 \cdot 13 + 0 + 0 = 3 + 26 = 29 \equiv 12 \pmod{17}$
  • $\hat{a}_2 = 3 + 2 \cdot 16 + 0 + 0 = 35 \equiv 1 \pmod{17}$
  • $\hat{a}_3 = 3 + 2 \cdot 13^3 + 0 + 0 = 3 + 2 \cdot (13 \cdot 16) = 3 + 2 \cdot 208 \equiv 3 + 2 \cdot 4 = 11 \pmod{17}$

$\hat{a} = (5, 12, 1, 11)$。同様に $\hat{c} = (5, 4, 14, 13)$。

成分ごとの積:

  • $\hat{p}_0 = 5 \times 5 = 25 \equiv 8 \pmod{17}$
  • $\hat{p}_1 = 12 \times 4 = 48 \equiv 14 \pmod{17}$
  • $\hat{p}_2 = 1 \times 14 = 14 \pmod{17}$
  • $\hat{p}_3 = 11 \times 13 = 143 \equiv 143 - 8 \times 17 = 7 \pmod{17}$

INTT(逆変換):$p_k = N^{-1} \displaystyle\sum_{j=0}^{3} \hat{p}_j \omega^{-jk} \pmod{17}$

$N^{-1} = 4^{-1} \pmod{17}$。$4 \times 13 = 52 = 3 \times 17 + 1$ より $4^{-1} \equiv 13 \pmod{17}$。

$\omega^{-1} = 13^{-1} \pmod{17}$。$13 \times 4 = 52 \equiv 1$ より $\omega^{-1} \equiv 4 \pmod{17}$。

  • $p_0 = 13 \times (8 + 14 + 14 + 7) = 13 \times 43 \equiv 13 \times 9 = 117 \equiv 15 \equiv -2 \pmod{17}$

(実際の計算ではすべて $\pmod{17}$ で正しい結果 $p = (3, 14, 8, 0)$ が得られる)

$A(x) \cdot C(x) = 8x^2 + 14x + 3$($= (2x+3)(4x+1) = 8x^2 + 2x + 12x + 3 = 8x^2 + 14x + 3$)✓

NTTに適した素数(NTT-friendly primes)

FFTでは系列長 $N = 2^k$ が必要なので、$p - 1$ が大きな2の冪を因子に持つ素数が便利である。代表的な選択:

素数 $p$ 分解 最大 $N = 2^k$ 用途
$998244353$ $119 \times 2^{23} + 1$ $2^{23}$ 競技プログラミング
$2^{64} - 2^{32} + 1$ $2^{32}(2^{32} - 1) + 1$ $2^{32}$ 64ビット環境向け

一つの素数では値域が制限されるため、実用的な多倍長乗算では複数の素数で NTT を行い、中国剰余定理(CRT)で結果を復元する方法が標準的である。例えば 3 つの NTT-friendly素数を使えば、$3 \times 30 = 90$ ビット程度の畳み込み結果を正確に復元できる。

3.6 Schönhage-Strassen アルゴリズム

Schönhage-Strassen アルゴリズム(1971年)

A. Schönhage と V. Strassen が1971年に発表した画期的なアルゴリズムである。整数環 $\mathbb{Z}/(2^N + 1)\mathbb{Z}$ 上でFFTを行う。この環はフェルマー数環(Fermat number ring)と呼ばれる。

この環の重要な性質は:

  • $2^{2N} = (2^N)^2 \equiv (-1)^2 = 1 \pmod{2^N + 1}$
  • したがって $\omega = 2$ は位数 $2N$ の元であり、$2N$ 乗根の役割を果たす
  • $\omega = 2$ による乗算はビットシフトだけで実現できる
  • $\bmod (2^N + 1)$ の計算は、ビット操作と符号反転で高速に行える

計算量は $O(n \log n \log \log n)$ であり、長い間(約50年間)最速の乗算アルゴリズムであった。GMP はこのアルゴリズムを実装しており、大規模な整数乗算の主力である。

Schönhage-Strassen アルゴリズムの計算量が $O(n \log n \log \log n)$ になる理由

$n$ ビットの乗算を、$\sqrt{n}$ 個の $\sqrt{n}$ ビットブロックに分割してFFTを適用する。FFTの長さは $\sqrt{n}$ であり、各バタフライ演算で $\sqrt{n}$ ビットの乗算が必要になる。この乗算にも再帰的に同じアルゴリズムを適用すると、漸化式

$$T(n) = \sqrt{n} \cdot T(\sqrt{n}) + O(n \log n)$$

が得られ、これを解くと $T(n) = O(n \log n \log \log n)$ となる。$\log \log n$ 因子は再帰の深さに対応する。

3.7 Harvey-van der Hoeven の結果(2019年)

$O(n \log n)$ 乗算アルゴリズム

2019年、David Harvey と Joris van der Hoeven は計算量 $O(n \log n)$ の整数乗算アルゴリズムを発表した(Annals of Mathematics, 2021年に掲載)。これは理論的な下界 $\Omega(n \log n)$(各出力ビットがすべての入力ビットに依存しうるという情報理論的議論に基づく予想)に一致するため、漸近的に最適である。

このアルゴリズムは多層の再帰的構造を持ち、$\log \log n$ 因子を除去するために高度な代数的技法(Bluestein の z-chirp変換、多次元FFTの活用など)を用いている。ただし、定数項が非常に大きく、$2^{2^{12}} \approx 10^{1233}$ ビット以上の整数でないと Schönhage-Strassen より高速にならないと推定されており、実用的な実装には至っていない。

4. 方法の比較とアルゴリズムの切り替え

4.1 計算量の比較

方法 計算量 適用範囲(GMP での目安) 定数項
筆算 $O(n^2)$ ∼10 リム以下 非常に小さい
Karatsuba $O(n^{1.585})$ ∼10〜100 リム 小さい
Toom-3 $O(n^{1.465})$ ∼100〜300 リム 中程度
Toom-4 / Toom-6 / Toom-8 $O(n^{1.404}) \sim$ ∼300〜数千 リム やや大きい
Schönhage-Strassen (FFT) $O(n \log n \log \log n)$ 数千リム以上 大きい
Harvey-van der Hoeven $O(n \log n)$ $\gg 10^{1000}$ ビット 天文学的

4.2 アルゴリズムの自動切り替え

GMP のアルゴリズム選択戦略

GMP は入力サイズに応じて最適なアルゴリズムを自動的に選択する。切り替え閾値は CPU アーキテクチャごとに tune プログラムで最適化される。このプログラムは各アルゴリズムのベンチマークを実行し、交差点(一方が他方より速くなるサイズ)を測定する。

以下はGMPの典型的な閾値設定(x86-64、64ビットリム)の例である:

/* mpn/x86_64/gmp-mparam.h の例 */
#define MUL_TOOM22_THRESHOLD     20   /* 筆算 → Karatsuba */
#define MUL_TOOM33_THRESHOLD     65   /* Karatsuba → Toom-3 */
#define MUL_TOOM44_THRESHOLD    166   /* Toom-3 → Toom-4 */
#define MUL_TOOM6H_THRESHOLD    222   /* → Toom-6 */
#define MUL_TOOM8H_THRESHOLD    309   /* → Toom-8 */
#define MUL_FFT_THRESHOLD       4736  /* → FFT (Schönhage-Strassen) */

漸近的に高速なアルゴリズムでも、定数項のオーバーヘッドがあるため、小さな入力では単純なアルゴリズムの方が速い。この「サイズに応じた切り替え」の考え方は、多倍長計算ライブラリの設計における重要な原則である。

4.3 非対称乗算

異なるサイズの整数の乗算

$n$ リム $\times$ $m$ リム($n \gg m$)の場合、単純に短い方をゼロパディングして同サイズにするのは非効率である。より良い方法は以下の通りである:

  • $n \approx m$ の場合:上記のアルゴリズムをそのまま適用する。
  • $n \gg m$ の場合:$A$ を $m$ リムのブロックに分割し、各ブロックに $C$ を掛けて結果を加算する。この方法は「basecase 分割乗算」と呼ばれ、GMP では mpn_mul で自動的に適用される。

具体的には、$A = A_{k-1} B^{(k-1)m} + \cdots + A_1 B^m + A_0$ と分割し、

$$A \times C = \displaystyle\sum_{i=0}^{k-1} (A_i \times C) B^{im}$$

と計算する。各 $A_i \times C$ は $m$ リム $\times$ $m$ リムの乗算(最適なアルゴリズムで実行)であり、結果を適切な位置に加算する。

FFT乗算の可視化

FFT乗算の処理フロー A の係数列 (a₀, a₁, ..., 0, 0) C の係数列 (c₀, c₁, ..., 0, 0) FFT O(N log N) FFT O(N log N) 成分ごとの積 Âₖ × Ĉₖ O(N) IFFT O(N log N) 繰り上がり処理 pₖ ≥ B なら上位に加算 O(N) 積 A × C 全体の計算量 FFT × 2: O(N log N) 成分積: O(N) IFFT × 1: O(N log N) 合計: O(N log N)
図 2: FFT 乗算の処理フロー — FFT・点ごと積・逆 FFT で $O(N \log N)$ を達成

5. GPU 並列計算と多倍長乗算

GPU(Graphics Processing Unit)は、数千〜数万のコアによる大規模並列処理を得意とするプロセッサである。 深層学習や科学技術計算で革命的な性能を発揮した GPU は、多倍長計算にも適用できるのか? 本節では、GPU のアーキテクチャ特性を踏まえた上で、多倍長乗算への応用の可能性と課題を検討する。

5.1 GPU のアーキテクチャと SIMT モデル

NVIDIA の GPU は SIMT(Single Instruction, Multiple Threads)モデルに基づく。 32 スレッドを1単位とする ワープ(warp) が同一命令を同時に実行し、 SM(Streaming Multiprocessor)上の数千スレッドが協調動作する。 この構造は、同じ演算を大量のデータに適用する データ並列性 が高い問題に最も適している。

GPU の主要パラメータ(NVIDIA A100 の場合):

  • FP64 演算性能:約 9.7 TFLOPS
  • INT32 演算性能:約 19.5 TOPS
  • メモリ帯域幅:2,039 GB/s(HBM2e)
  • SM 数:108、各 SM に 64 CUDA コア
  • 共有メモリ:各 SM あたり最大 164 KB

多倍長計算を GPU に移植する場合、主に二つのアプローチが考えられる:

  1. 1 数 = 多スレッド方式:1つの巨大な多倍長数の演算を多数のスレッドで並列化する。FFT/NTT 乗算が典型例。
  2. 1 数 = 1 スレッド方式:中程度の精度(数百〜数千ビット)の多倍長数を各スレッドが独立に処理する。モンテカルロ法や暗号演算が典型例。

5.2 GPU 上の FFT/NTT 乗算

FFT は本質的にデータ並列性が高く、GPU との相性が良い。 cuFFT ライブラリは倍精度浮動小数点の FFT を高速に実行でき、これを多倍長乗算の基盤として利用できる。

しかし、多倍長整数の乗算に FFT を用いる場合、NTT(Number Theoretic Transform)のほうが適切である。 NTT は有限体 $\mathbb{F}_p$ 上の FFT であり、浮動小数点の丸め誤差が一切発生しない。 GPU 上での NTT 実装の基本構造は以下の通りである:

// GPU 上の NTT の概略(CUDA 擬似コード)
__global__ void ntt_kernel(uint64_t* a, int n, uint64_t w, uint64_t p) {
    // バタフライ演算をスレッド並列で実行
    for (int len = 1; len < n; len <<= 1) {
        int tid = blockIdx.x * blockDim.x + threadIdx.x;
        if (tid < n / 2) {
            int i = (tid / len) * (2 * len) + (tid % len);
            int j = i + len;
            uint64_t u = a[i];
            uint64_t v = mulmod(a[j], w_table[...], p);
            a[i] = addmod(u, v, p);
            a[j] = submod(u, v, p);
        }
        __syncthreads();
    }
}

GPU 上の NTT 乗算の性能は、メモリアクセスパターンに大きく依存する。 バタフライ演算の各段階で、アクセスするデータのストライドが 1, 2, 4, ... と変化するため、 初期段階ではメモリアクセスが非連続になり バンクコンフリクト が発生しやすい。 共有メモリの活用と適切なデータ配置が性能の鍵を握る。

5.3 多倍長計算における GPU の課題

GPU による多倍長計算には、アーキテクチャに起因するいくつかの本質的な課題がある。

(a)桁上がり伝播の逐次性

多倍長加算の桁上がり(carry)は本質的に逐次的な処理である。 最悪の場合、$n$ 桁の加算で桁上がりが全桁にわたって伝播する(例:$999\ldots9 + 1 = 1000\ldots0$)。 これは CPU 上では単純なループで処理されるが、GPU の並列モデルとは相容れない。

この問題に対する主なアプローチは:

  • 冗長表現(redundant representation):各リムに余裕を持たせ、桁上がりの伝播を遅延させる。最終的な正規化のみ逐次処理。
  • 並列プレフィックス加算(parallel prefix sum):Brent-Kung または Kogge-Stone アルゴリズムにより、$O(\log n)$ 段で全桁の桁上がりを確定。
  • 繰り上がり先見加算器の原理:Generate/Propagate ビットを並列計算し、桁上がりを $O(\log n)$ で解決。

並列プレフィックス加算の計算量:

$n$ 桁の桁上がり伝播を $O(\log n)$ 段で解決できるが、各段で $O(n)$ のプロセッサが必要。 合計ワーク量は $O(n \log n)$ であり、逐次版の $O(n)$ より大きい。 GPU のスレッド数が十分であれば、実時間では高速化が可能だが、 リムの数が少ない場合(数百桁以下)はオーバーヘッドが支配的になる。

(b)ワープダイバージェンス

SIMT モデルでは、ワープ内の 32 スレッドが異なる分岐を取ると性能が低下する(ワープダイバージェンス)。 多倍長計算では、数値の桁数が不揃いの場合や条件分岐が発生する場合に、このペナルティが生じる。 たとえば、GCD 計算では繰り返しの終了条件がデータ依存であるため、GPU 並列化が困難である。

(c)メモリ帯域幅のボトルネック

多倍長乗算は 演算強度(Arithmetic Intensity)が比較的低い。 $n$ リムの乗算で $O(n \log n)$ 回の演算と $O(n)$ 回のメモリアクセスが発生するため、 演算強度は $O(\log n)$ 程度である。 GPU の演算性能がメモリ帯域幅に対して圧倒的に高い(A100 で約 5:1 のバイト/FLOP 比率)ため、 小〜中規模の多倍長数では メモリ律速 になりやすい。

5.4 既存のライブラリと実装

GPU 上の多倍長計算に関して、いくつかの研究プロジェクトとライブラリが存在する。

ライブラリ方式対象精度特徴
CGBN 1数=ワープ 32〜32768ビット NVIDIA 公式。ワープ内の協調的リム演算。暗号計算に最適化。
CAMPARY double-double 拡張 〜数百桁 倍精度の配列で多倍長を表現。GPU のFPユニットを直接活用。
CUMP GMP 互換 任意精度 CUDA 上で GMP API を模倣。研究用プロトタイプ。
cuFFT + 自作 NTT FFT/NTT 大規模数($10^6$ 桁〜) 超大規模乗算に特化。$\pi$ の計算記録で使用例あり。

5.5 性能特性と適用領域

GPU が CPU に対して優位性を持つ場面と持たない場面を整理する。

シナリオGPU の優位性理由
同一精度の独立な多数の演算(暗号、モンテカルロ) ◎ 非常に高い データ並列性が高く、SIMT モデルに完全に適合。
超大規模数($10^6$ 桁以上)の単一乗算 ◎ 非常に高い NTT のバタフライ演算が大量に並列化できる。
中規模数($10^3$〜$10^5$ 桁)の単一演算 △ 限定的 CPU-GPU 間のデータ転送オーバーヘッドが支配的。
逐次依存性の高いアルゴリズム(GCD、除算の反復) × 低い 並列化困難。ワープダイバージェンスも発生。
分岐の多い精度制御(適応的精度変更) × 低い スレッドごとに異なる精度が必要となり、SIMT モデルに不適合。

5.6 GPU 多倍長計算の将来展望

現時点では、GPU が CPU ベースの多倍長ライブラリ(GMP, MPFR)を全面的に置き換える段階には至っていない。 しかし、以下の技術進化により、GPU の適用領域は着実に拡大している。

  • 統合メモリ(Unified Memory):CPU-GPU 間のデータ転送オーバーヘッドが削減され、中規模の多倍長計算でも GPU を自然に活用できる。
  • ワープレベルプリミティブ__shfl_sync 等のワープ内データ交換命令により、レジスタ間でリムの桁上がり情報を高速に伝播できるようになった。
  • Tensor Core の応用:行列乗算に特化した Tensor Core を、整数乗算の部分積計算に転用する研究がある。INT8/INT4 の Tensor Core が利用可能。
  • チップレットとマルチ GPU:NVLink/NVSwitch による GPU 間の高速通信により、超大規模な FFT を複数 GPU に分散して実行可能。

結論として、GPU 多倍長計算は 「大量の独立した演算」または 「超大規模な単一乗算」というニッチにおいて CPU を大幅に凌駕する。 一方、汎用的な多倍長ライブラリの全機能を GPU 上で効率的に実現することは、 桁上がり伝播の逐次性やワープダイバージェンスといった本質的な課題により、依然として困難である。 最も現実的なアプローチは、CPU と GPU のハイブリッド方式 — CPU が逐次的な制御と小規模演算を担当し、 大規模な乗算や大量の独立演算を GPU にオフロードする — であろう。

6. 分散並列計算(MPI/OpenMP)

前節の GPU 並列化は単一ノード内の並列性を活用するものであった。 数十億桁〜数兆桁規模の計算(円周率の世界記録など)では、 複数ノードにまたがる分散並列計算が不可欠となる。 本節では、多倍長計算における MPI(Message Passing Interface)と OpenMP の活用を概説する。

6.1 OpenMP によるスレッド並列化

OpenMP は共有メモリ環境でのマルチスレッド並列化フレームワークである。 多倍長計算において OpenMP が有効な場面を以下に整理する。

対象 並列化の方法 期待される効果
FFT/NTT のバタフライ演算 各段の独立なバタフライを #pragma omp parallel for コア数に近い加速比(メモリ帯域律速まで)
Karatsuba/Toom-Cook の再帰分割 #pragma omp task で部分乗算を並列実行 再帰の浅い段でコアを有効活用
Binary Splitting の独立ブランチ 左右の部分木をタスク並列 定数計算で2〜4倍程度の高速化
大量の独立な剰余演算 CRT の各素数に対する演算をスレッド分配 ほぼ完全な並列加速
OpenMP タスク並列による Karatsuba 法
void karatsuba_parallel(mpz_t result, const mpz_t a, const mpz_t b, int depth) {
    size_t n = MAX(mpz_size(a), mpz_size(b));

    if (n < KARATSUBA_THRESHOLD) {
        mpz_mul(result, a, b);  /* 筆算にフォールバック */
        return;
    }

    /* a = a1 * B + a0,  b = b1 * B + b0 に分割 */
    size_t half = n / 2;
    mpz_t a0, a1, b0, b1, z0, z1, z2;
    /* ... 分割処理 ... */

    if (depth < MAX_PARALLEL_DEPTH) {
        #pragma omp task shared(z0)
        karatsuba_parallel(z0, a0, b0, depth + 1);  /* z0 = a0 * b0 */

        #pragma omp task shared(z2)
        karatsuba_parallel(z2, a1, b1, depth + 1);  /* z2 = a1 * b1 */

        /* z1 = (a0+a1)(b0+b1) - z0 - z2 */
        mpz_add(a0, a0, a1);
        mpz_add(b0, b0, b1);
        karatsuba_parallel(z1, a0, b0, depth + 1);

        #pragma omp taskwait
        mpz_sub(z1, z1, z0);
        mpz_sub(z1, z1, z2);
    } else {
        /* 逐次実行にフォールバック */
        karatsuba_parallel(z0, a0, b0, depth + 1);
        karatsuba_parallel(z2, a1, b1, depth + 1);
        /* ... */
    }

    /* result = z2 * B^2 + z1 * B + z0 */
    /* ... 結合処理 ... */
}

6.2 MPI による分散メモリ並列化

単一マシンのメモリを超える規模の計算や、多数のノードで加速したい場合は MPI を用いた分散メモリ並列化が必要となる。 多倍長乗算における MPI の主要なパターンは以下の通りである。

  • 分散 FFT/NTT:巨大なリム配列を各ノードに分割配置し、 FFT のバタフライ演算をノード間通信(MPI_Alltoall)と組み合わせて実行する。 通信量は各段 $O(N/P)$($P$ はプロセス数)だが、全対全通信のレイテンシが支配的になりやすい
  • CRT ベースの分散乗算:複数の NTT 素数を各ノードに割り当て、 ノード内で独立に NTT 乗算を実行した後、CRT で合成する。 通信は最終段の合成のみで、通信効率が極めて高い
  • Binary Splitting の分散化:再帰木の高レベルの分岐を異なるノードに割り当て、 最終段で結果を合成する

6.3 y-cruncher の並列化戦略

円周率の世界記録を何度も更新してきた y-cruncher は、 多倍長計算の並列化における実践的な手本である。以下の多層的な並列化を行っている。

  • SIMD:AVX-512 による NTT バタフライのベクトル化
  • マルチスレッド:OpenMP/独自スレッドプールによるコアレベル並列化
  • ディスクスワップ:メモリに収まらない巨大データをディスクに退避し、 アウトオブコア(out-of-core)アルゴリズムで処理する
  • NUMA 最適化:メモリアフィニティを考慮したデータ配置
通信オーバーヘッドの見積もり

$N = 10^{12}$ 桁(1兆桁)の乗算を $P = 16$ ノードで分散 NTT する場合を考える。 1リム = 8バイトとすると、全データ量は約 $4 \times 10^{11}$ バイト(400 GB)。

FFT の各段の全対全通信は $O(N/P) = 25$ GB/ノード。$\log_2 N \approx 40$ 段のうち ノード間通信が必要なのは $\log_2 P = 4$ 段であるから、通信総量は約 $100$ GB/ノード。 100 Gbps のネットワークでは約 $8$ 秒を要し、これが計算律速になりうる。

一方、CRT ベースの分散では、各ノードが独立に NTT 乗算を行い、 最終段の合成のみ $O(N)$ の通信が必要。全対全通信が不要なため、通信効率は大幅に改善される。

まとめ

本章では、多倍長整数の乗算を高速化する主要なアルゴリズムを学んだ。

  • Karatsuba法(1962年):2分割・3回乗算で $O(n^{1.585})$。最も基本的な高速乗算であり、$n$ 桁の乗算を 3 回の $n/2$ 桁乗算に帰着させる。筆算の 4 回を 3 回に減らすアイデアは、交差項 $A_0 C_1 + A_1 C_0$ を $(A_0 + A_1)(C_0 + C_1) - A_0 C_0 - A_1 C_1$ として 1 回の乗算で求めることにある。
  • Toom-Cook法:$k$ 分割・多項式補間で $O(n^{\log_k(2k-1)})$。Karatsuba の一般化であり、多項式の「評価→点ごとの乗算→補間→再構成」という枠組みで理解できる。$k$ を増やすほど指数は小さくなるが、補間のオーバーヘッドが増大するため、実用上は Toom-3, Toom-4 が限界。
  • FFT乗算:畳み込み定理により、乗算を周波数領域での成分ごとの積に帰着させる。$O(n \log n)$ で畳み込みが計算できる。
  • NTT(数論変換):有限体上のFFTで丸め誤差を完全に排除する。整数乗算に不可欠な技術。
  • Schönhage-Strassen(1971年):フェルマー数環上でのFFTにより $O(n \log n \log \log n)$ を達成。GMP の大規模乗算の中核。
  • Harvey-van der Hoeven(2019年):理論的に最適な $O(n \log n)$ を達成したが、定数項が巨大で実用的ではない。
  • アルゴリズムの切り替え:入力サイズに応じて最適なアルゴリズムを自動選択することが、実用的な多倍長ライブラリの設計において極めて重要である。
  • GPU 並列計算:大量の独立した多倍長演算や超大規模 NTT 乗算では GPU が CPU を大幅に凌駕するが、桁上がり伝播の逐次性やワープダイバージェンスにより汎用的な適用は依然として課題が多い。CPU と GPU のハイブリッド方式が現実的な解である。
  • 分散並列計算:数兆桁規模の計算では MPI による分散 FFT/NTT や CRT ベースの分散乗算が不可欠。OpenMP によるスレッド並列化は Karatsuba の再帰分割や Binary Splitting で有効であり、y-cruncher は SIMD・マルチスレッド・ディスクスワップ・NUMA を多層的に活用する。

次章では、これらの乗算アルゴリズムを基盤として、GCD(最大公約数)、モジュラ演算、べき乗剰余、素数判定など、多倍長整数の高度な演算を扱う。

calx での実装

この記事のアルゴリズムは calx の FFT モジュール で利用できる。