【非対称鍵暗号】CPU・AVX2・GPUで挑むハッキング実験と量子コンピュータがRSAを一撃で破る理

私たちの日常の背後で、クレジットカード決済から機密データの通信まで、あらゆるインターネットセキュリティを支え続けている守護神――それが非対称鍵暗号(公開鍵暗号)です。

1970年代の誕生以来、この技術は安全な通信の礎であり続けました。しかし今、「量子コンピュータ」という究極の計算機の出現により、その安全性が根底から覆されようとしています。

本記事では、非対称鍵暗号の代名詞である「RSA暗号」をテーマに、その本質から、暗算レベルの基礎、そしてCPU・AVX2(SIMD)・GPU(OpenCL)を駆使したハッキング(素因数分解)の実験コードと測定結果を解説します。
さらに、量子コンピュータがなぜこの暗号を一撃で破れるのかを「頭の中で暗算できるシミュレーション」で紐解き、次世代の「耐量子暗号(PQC)」の最前線へと迫ります。


1. 非対称鍵暗号の由来と本質:トラップドア(落とし戸)という発想

暗号の歴史的パラダイムシフト

非対称鍵暗号が登場する前、世界の暗号はすべて「共通鍵暗号」でした。送り手と受け手がまったく同じ鍵を持ち、それをあらかじめ秘密裏に共有しておく必要があったのです。
しかし、この方式には致命的な弱点がありました。「そもそも、どうやって最初の鍵を相手に安全に届けるのか?」という「鍵配送問題」です。インターネットのように、一度も会ったことのない世界中のサーバーと安全に通信したい現代において、共通鍵の事前共有は不可能です。

この問題を解決したのが、1976年にディフィー(Diffie)とヘルマン(Hellman)が提唱した概念であり、1977年にリベスト(Rivest)、シャミア(Shamir)、エーデルマン(Adleman)の3人によって具体化された「RSA暗号」です。

本質は「トラップドア関数(落とし戸付き一方向性関数)」

非対称鍵暗号の本質は、非常にシンプルな数学的性質にあります。それは「行くのは簡単だが、戻るのは極めて困難。ただし、抜け道(落とし戸=秘密の鍵)を知っていれば簡単に戻れる」という関数です。

もっとも直感的な例が「巨大な素数の掛け算」です。

  • 一方向(簡単): 2つの素数 pq を掛け合わせて n = p × q を計算することは、小学校の筆算レベルで一瞬で終わります。
  • 逆方向(困難): 与えられた巨大な数 n を元の素数 pq に分解する(素因数分解)には、手がかりが一切ないため、総当たりに近い途方もない計算が必要になります。

この「素因数分解の困難さ」こそが、RSA暗号の安全性の強固な土台となっています。


2. 応用:暗号化とデジタル署名の対称性

非対称鍵暗号では、誰でも使える「公開鍵」と、自分だけが持つ「秘密鍵」のペアを作成します。この2つの鍵には面白い対称性があります。

① 暗号化(誰でも送れて、自分だけが読める)

Aさんへ秘密のメッセージを送りたい人は、Aさんの「公開鍵」を使ってメッセージを暗号化します。この暗号文は、世界中でAさんの「秘密鍵」を持つAさん本人しか復号できません。

② デジタル署名(自分だけが作れて、誰でも検証できる)

逆に、Aさんが「秘密鍵」を使ってメッセージを暗号化(署名)します。この暗号文は、Aさんの「公開鍵」を使えば誰でも復号できます。「公開鍵で復号できた」ということは、すなわち「Aさんの秘密鍵で作成された」という動かぬ証拠になり、本人の身元証明やデータの改ざん検知(署名の検証)に利用されます。


3. 暗算で体験するRSA暗号・復号・ハッキング

まずは、頭の中で追える「超小型のRSA暗号」を使って、暗号化、復号、そしてハッキング(素因数分解による解読)の工程を体験してみましょう。

鍵の作成(アルゴリズム)

  1. 素数を選ぶ:
    例として、極小の素数 p = 61, q = 53 を選びます。
  2. モジュラス n の計算:n = p × q = 61 × 53 = 3233
  3. オイラーのラムダ(またはファイ)関数 φ(n) の計算: n 未満で n と互いに素な整数の個数です。φ(n) = (p – 1)(q – 1) = 60 × 52 = 3120
  4. 公開指数 e の選択:φ(n) と互いに素である適当な数として、今回は e = 17 を選びます。
    • 公開鍵: (e, n) = (17, 3233)
  5. 秘密の鍵 d の導出:e × d ≡ 1 (mod φ(n)) となる d を「拡張ユークリッドの互除法」などで求めます。17 × d = 3120 の倍数 + 1計算すると、d = 2753 となります(17 × 2753 = 46801 = 15 × 3120 + 1)。
    • 秘密鍵: (d, n) = (2753, 3233)

暗号化と復号のデモ

平文(メッセージ)を m = 65 とします(条件:0 ≤ m < n)。

  • 暗号化: 公開鍵 (e, n) を使用します。c = me mod n = 6517 mod 3233これを計算すると、暗号文は c = 2790 になります。
  • 復号: 秘密鍵 (d, n) を使用します。m’ = cd mod n = 27902753 mod 3233これを計算すると、元の平文である m’ = 65 が見事に復元されます。

ハッカーの視点(解読の体験)

ハッカーは公開鍵 (e, n) = (17, 3233) と暗号文 c = 2790 を傍受しています。
ハッカーが秘密鍵 d を知るためには、φ(n) を知る必要があり、そのためには n = 3233 を素因数分解しなければなりません。

暗算でハッキングしてみましょう。
3233の平方根は √3233 ≈ 56.8 です。
56以下の奇数(3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53)で 3233 を順番に割っていきます。
すると、53 で割ったときにぴったり割り切れます(3233 ÷ 53 = 61)
これで p = 61, q = 53 を特定され、ハッキングは成功です。


4. 限界突破のハッキング実験(CPU vs AVX2 vs GPU)

暗算や紙とペンで解読できるのはせいぜい4桁(モジュラスが千の位)までです。では、桁数が上がっていくと計算時間はどう変化するのでしょうか?
C言語を用いて、実際に素因数分解(試し割り法)を行うベンチマークコードを作成し、通常のCPU、AVX2(SIMD高速化)、GPU(OpenCL)で解読速度の統計をとってみました。

実験1:汎用CPU(シングルスレッド・スカラー演算)

最も愚直に奇数で順番に割っていくコードです。

C
/* rsa_manual_cpu.c より抜粋:試し割り法 */
static u64 trial_division(u64 n) {
    if (n % 2 == 0) return 2;
    for (u64 d = 3; d * d <= n; d += 2) {
        if (n % d == 0) return d;
    }
    return 0; /* n自体が素数 */
}

【測定結果】桁数増加に対する所要時間(CPU)

合成数 n (2つの素数の積)発見した因数(小さい方)所要時間 (秒)
9,797 (4桁)970.000000
1,005,973 (7桁)9970.000000
99,799,811 (8桁)9,9730.000000
9,999,399,973 (10桁)99,9110.000000
999,985,999,949 (12桁)999,9830.000000
100,000,099,999,829 (15桁)9,999,9910.010000
9,999,999,599,999,923 (16桁)99,999,9890.148000

16桁(約253)を超えたあたりから、0.1秒以上の目に見える処理時間がかかり始めます。試し割り法では、計算量が最大で O(√n) で増えていくため、桁数に対して指数関数的に時間が増大します。

実験2:AVX2(SIMD)による高速化とその「罠」

CPUのSIMD命令(AVX2)を使い、1命令で4つの除数候補(d, d+2, d+4, d+6)を同時にチェックする並列コードです。浮動小数点演算(__m256d)を用いて剰余が0になるかを判定します。

C
/* rsa_manual_avx2.c より抜粋 */
static u64 trial_division_avx2(u64 n) {
    if (n % 2 == 0) return 2;
    double dn = (double)n;
    __m256d vn = _mm256_set1_pd(dn);
    __m256d vzero = _mm256_set1_pd(0.0);

    for (u64 d = 3; (double)d * (double)d <= dn; d += 8) {
        double arr[4] = { (double)d, (double)(d + 2), (double)(d + 4), (double)(d + 6) };
        __m256d vd   = _mm256_loadu_pd(arr);
        __m256d q    = _mm256_div_pd(vn, vd);      // 除算
        __m256d qf   = _mm256_floor_pd(q);        // 床関数
        __m256d prod = _mm256_mul_pd(qf, vd);
        __m256d rem  = _mm256_sub_pd(vn, prod);     // 剰余の算出
        __m256d cmp  = _mm256_cmp_pd(rem, vzero, _CMP_EQ_OQ);
        int mask = _mm256_movemask_pd(cmp);
        if (mask) {
            for (int i = 0; i < 4; i++) {
                if (mask & (1 << i)) return (u64)arr[i];
            }
        }
    }
    return 0;
}

【測定結果】CPUスカラー vs AVX2 vs AVX-512 比較(巨大合成数)

64ビット整数の表現限界に近い巨大な合成数 n = 18,446,744,030,759,878,681 (平方根 √n ≈ 4,294,967,291)を対象とし、CPUスカラー版の1回の計算に約5.0秒かかる高負荷な条件で、2回繰り返し(イテレーション) 実行した際の総所要時間を比較しました。

バージョン総所要時間 (秒)1回あたりの平均時間 (秒)速度倍率
① CPUスカラー版9.5770004.7885001.00x (基準)
② AVX2並列版 (4並列)16.7060008.3530000.58x (約1.7倍遅い)
③ AVX-512並列版 (8並列)10.2540005.1270000.93x (スカラーと同等〜僅かに遅い)

技術的な発見:なぜ並列化したのに遅くなったのか?

理論上は4並列・8並列と高速になるはずのSIMD版ですが、実測ではスカラー版より遅くなるという結果になりました。これにはアーキテクチャ上の明確な理由があります(後述のAVX-512の解説で詳述します)。


実験3:AVX-512(8並列)によるリベンジ

さらに多くの候補を一度に処理するため、512ビット幅のレジスタを備える最新の「AVX-512」命令を活用したバージョンを作成しました。倍精度浮動小数点(__m512d)で除算(商の算出)を行い、剰余の最終検証のみを64ビット整数ベクトル演算で行うハイブリッド判定を導入しています。

C
/* rsa_manual_avx512.c より抜粋(高精度ハイブリッド判定) */
static u64 trial_division_avx512(u64 n) {
    if (n % 2 == 0) return 2;
    double dn = (double)n;
    __m512d vn = _mm512_set1_pd(dn);
    __m512i vn_64 = _mm512_set1_epi64((long long)n);
    __m512i vzero_i = _mm512_setzero_si512();

    for (u64 d = 3; (double)d * (double)d <= dn; d += 16) {
        double arr[8] = {
            (double)d, (double)(d + 2), (double)(d + 4), (double)(d + 6),
            (double)(d + 8), (double)(d + 10), (double)(d + 12), (double)(d + 14)
        };
        __m512d vd   = _mm512_loadu_pd(arr);
        __m512d q    = _mm512_div_pd(vn, vd);
        __m512d qf   = _mm512_floor_pd(q);

        // 32ビット整数に一度変換し、64ビットへ符号拡張(商qfと除数vdは共に32ビット素数以下なので完全に正確)
        __m256i vqf_32 = _mm512_cvttpd_epi32(qf);
        __m512i vqf = _mm512_cvtepi32_epi64(vqf_32);

        __m256i vd_32 = _mm512_cvttpd_epi32(vd);
        __m512i vd_64 = _mm512_cvtepi32_epi64(vd_32);

        // 正確な64ビット乗算
        __m512i vprod = _mm512_mul_epu32(vqf, vd_64);

        // vrem = n - qf * d
        __m512i vrem = _mm512_sub_epi64(vn_64, vprod);

        // 剰余が0であるか比較(マスク判定)
        __mmask8 cmp = _mm512_cmpeq_epi64_mask(vrem, vzero_i);
        if (cmp) {
            for (int i = 0; i < 8; i++) {
                if (cmp & (1 << i)) return (u64)arr[i];
            }
        }
    }
    return 0;
}

アーキテクチャ解説:なぜ桁数を増やしても並列命令はスカラーに勝てないのか?

読者の中には、「AVX512やAVX2は、計算規模(桁数)が小さいためにオーバーヘッドで遅くなったのでは?」と推測された方も多いでしょう。
しかし、今回の実験では、1回あたりの計算ループ数が約 21.4億回(CPUが本気で数秒間ぶん回し続ける規模) という巨大な負荷を与えました。オーバーヘッド(ループの開始やデータのロードなど)の割合は、全体の計算時間(5秒以上)に対してほぼゼロに等しい極小のレベルです。

それにもかかわらず、8並列のAVX-512をもってしても 0.93x(スカラーに僅かに敗北)、4並列のAVX2では 0.58x(大幅に敗北)という結果になりました。
ここから導き出される結論は、遅い原因が「桁数が少ないためのオーバーヘッド」ではなく、「x86-64 CPUにおけるベクトル除算とスカラー整数除算の、ハードウェア実行効率の決定的な差」にあるということです。

  1. スカラー専用除算回路の極めて高いスループット:
    現代のCPU内部には、スカラーの %(剰余)や /(除算)を処理するための専用の高速なハードウェア除算回路が用意されています。スカラー版はこの高速な専用回路をダイレクトに叩くため、非常に高い効率で剰余が計算されます。
  2. 64ビット整数除算のベクトル命令の欠如:
    AVX2やAVX-512などのベクトル演算ユニットには「64ビット整数の除算・剰余」を一発で計算するベクトル命令が存在しません。
    そのため、並列試し割りを行うには、値を一度 double(倍精度浮動小数点)に変換した上でベクトル除算(_mm512_div_pd)を行い、それを floor 命令で丸めて整数値へ切り下げ、さらに元の除数を掛け算して引き算を行う、という「何段階もの重い浮動小数点ベクトル命令の組み合わせ」で擬似的に剰余を算出しなければなりません。

つまり、いくら計算規模(ループ回数)を増やしても、ループ1周あたりの「擬似剰余演算の重さ」というアーキテクチャ上の制約があるため、4並列や8並列といった並列度では、スカラーの高速な専用除算回路の壁を越えることができないのです。


実験4:GPU(OpenCL)による真の超並列ハッキング

次に、GPUの数万スレッドを使い、除数候補を一斉に割り当てて判定するOpenCLバージョンで検証します。
GPU側へのメモリ転送オーバーヘッドや結果バッファのメモリ容量を最適化するため、見つかった因数のみを出力バッファの先頭アドレスに書き込む省メモリカーネルを実装しました。

C
/* rsa_manual_opencl.c:GPU側カーネル */
__kernel void trial_division(__global const ulong *n_ptr,
                              __global const ulong *base_ptr,
                              __global ulong *result) {
    ulong n = *n_ptr;
    ulong base = *base_ptr;
    size_t gid = get_global_id(0);
    ulong d = base + gid * 2; /* 奇数候補のみを並列走査 */
    if (d >= 3 && n % d == 0) {
        *result = d; /* 発見した因数を直接上書き */
    }
}

【測定結果】GPU(OpenCL)による検証

CPU、AVX2、AVX-512と同一の条件(n = 18,446,744,030,759,878,681、2イテレーション)で測定を行いました。

  • GPU並列ワークアイテム数(1回あたり): 2,147,483,645 スレッド (約21.4億)
  • GPU並列総実行時間(2回合計): 0.713846 秒
  • 1回あたりの平均時間: 0.356923 秒 (約357ミリ秒)
  • CPUスカラー比: 約 13.4倍 高速化

CPUスカラーで1回あたり約4.8秒かかっていた超巨大な計算範囲を、GPUはなんとわずか 357ミリ秒 (0.356923秒) で終わらせました。アトミック書き込みによる省メモリ最適化を導入したことで、21億を超える巨大なスレッドを立ち上げてもGPUメモリを一切圧迫せず、その圧倒的な演算スループットを完全に引き出しています。

結論:通常の計算機で現代の鍵長(RSA-2048)を破るコスト

GPUによって14〜15桁の分解が一瞬で行えることが分かりました。しかし、現在使われている RSA-2048 は、2進数で2048桁、10進数では約617桁の巨大な数です。

もしRSA-2048を、今回実験した試し割り法で解読しようとした場合、チェックすべき因数の範囲は最大で √n ≈ 10308 個になります。

  • 地球上のすべての原子の数(約 1050 個)のスーパーコンピュータを並べる。
  • 各マシンが1秒間に100京回(1018回)の割り算を実行する。
  • これを宇宙が誕生してから現在までの時間(約 138億年 ≈ 4 × 1017 秒)実行し続ける。

これほどの資源を投入しても、探索できる範囲は 1050 × 1018 × 1017 = 1085 通りに過ぎません。目標の 10308 には掠りもしないのです。
より洗練された「一般数体篩法(GNFS)」というアルゴリズムを使っても、解読にかかる時間は宇宙の寿命を遥かに超えます。これが「非対称鍵暗号は絶対に破られない」と信じられてきた根拠でした。

――そう、「量子コンピュータ」が登場するまでは。


5. 量子コンピュータはなぜ暗号を一撃で破れるのか?

量子コンピュータは、「計算がめちゃくちゃ速いスーパーコンピュータ」ではありません。まったく異なる物理法則(量子力学)を計算の原理に利用する、別次元の計算機です。

量子コンピュータがRSA暗号を一撃で破るアルゴリズムを「Shor(ショア)のアルゴリズム」と呼びます。その本質を、数式抜きで「頭の中で暗算できるシミュレーション」を使って体感してみましょう。

暗号破りの本質は「周期性(リズム)探し」

実は、数学的なトリックを使うと、「素因数分解をする」という問題は、「ある数式の『繰り返し周期』を見つける」という問題に完全に変換できます。

この数式は非常にシンプルです(底を a とします)。

f(x) = ax mod N

手作業でシミュレーションしてみよう!

モジュラスを N = 15 (分解したい数)とし、適当な底として a = 7 を選びます。
x = 0 から順番に値を代入し、7x mod 15 を計算してその余りのリズムを追いかけてみましょう。

  • x = 0 → 70 mod 15 = 1
  • x = 1 → 71 mod 15 = 7
  • x = 2 → 72 mod 15 = 49 ÷ 15 = 3 余り 4
  • x = 3 → 73 mod 15 = 343 ÷ 15 = 22 余り 13
  • x = 4 → 74 mod 15 = 2401 ÷ 15 = 160 余り 11に戻った!
  • x = 5 → 75 mod 15 = 16807 mod 15 = 7
  • x = 6 → 76 mod 15 = 4
  • x = 7 → 77 mod 15 = 13

余りのリストを書き出してみると、次のようになります。

[1, 7, 4, 13, 1, 7, 4, 13, 1, 7, 4, 13, …]

この数値の列には、「4」というきれいな繰り返し周期(リズム)があることが分かります。
周期 r = 4 を手に入れました。

周期 r から因数分解への魔法

周期 r = 4 さえわかれば、簡単な計算(最大公約数:GCD)だけで元の素因数(3と5)が導き出せます。

  1. ar/2 ± 1 を計算する: 74/2 = 72 = 49 なので、
    • 49 – 1 = 48
    • 49 + 1 = 50
  2. それぞれと N=15 との最大公約数(互除法で一瞬で解ける)を求める:
    • gcd(48, 15) = 3
    • gcd(50, 15) = 5

見事に 15 = 3 × 5 という素因数分解が完了しました!

なぜ量子コンピュータなら「一撃」なのか?

上記のシミュレーションは簡単でしたが、N が2048ビットになると、周期 r は天文学的な大きさになります。

  • 古典コンピュータの場合:
    周期を見つけるために、x = 0, 1, 2, 3… と順番に計算していく必要があります。何兆回も計算したあとでようやく「あ、ここで元の数字に戻った!」と気づくわけです。この「順番に数値を試す」アプローチは、どれだけコンピュータが並列化されても途方もない時間がかかります。
  • 量子コンピュータの場合(波の干渉): 量子コンピュータは、「量子重ね合わせ」という状態を作ることができます。
    1. まず、x の値すべて(例えば数億通り)を同時に重ね合わせた状態を作ります。
    2. その重ね合わせ状態に対して、式 f(x) = ax mod Nたった1回計算します。これにより、すべての計算結果が重ね合わさった「波の海」が出来上がります。
    3. この波の海に対して「量子フーリエ変換(QFT)」という操作を行います。これは、物理的な「プリズム」が光を波長(周波数)ごとに分けるような処理です。
    4. QFTをかけると、量子力学的な波の「干渉効果」によって、周期 r の倍数に対応する信号だけが互いに強め合い、それ以外のデタラメな信号は打ち消し合って完全に消滅します。
    5. 最後にその状態を測定すると、まるで手品のように、周期を示す鋭いピーク(周波数)だけが一撃で観測されます。

古典コンピュータが数億ステップを順に実行しなければならないところを、量子コンピュータは「全ての可能性を重ね合わせた波を干渉させ、一回のQFTと測定で周期を炙り出す」ため、桁数に対してO(L3)(多項式時間)という圧倒的な速さで処理が完了するのです。


6. 耐量子暗号(PQC)への移行と「ビット数を増やすだけではダメな理由」

「量子コンピュータがそんなに強いなら、RSAの鍵のビット数を今の2048ビットから1万ビット、あるいは10万ビットに増やせばいいのではないか?」という疑問が当然湧くでしょう。

疑問:単純に鍵のビット数を増やして対抗できるか?

結論から言うと、対抗できません。

量子コンピュータでShorのアルゴリズムを実行した際の計算コストは、ビット数(鍵長)を L とすると、おおよそ O(L3) のスケールでしか増加しません。

もし鍵の長さを 2倍(4096ビット) に増やした場合:

  • 量子コンピュータの計算時間は、23 = 8倍 にしかならない。
  • 鍵の長さを 10倍 に増やしても、計算時間は 103 = 1000倍 になる程度。現在の超高速なナノ秒単位の演算世界において、1000倍程度の増加は「一瞬」が「わずか数秒」になるだけで、安全性の防壁にはなり得ません。

逆に、鍵長を極端に増やすと、古典コンピュータ側で行う通常の暗号化・復号の処理負荷(こちらも多項式時間ですが次数があります)が急激に増大し、スマートフォンやIoT機器の通信が遅れて実用に耐えなくなってしまいます。

つまり、鍵長を引き上げるだけの防御策は「防御側が先に自滅する」ため、本質的な解決になりません。

耐量子暗号(PQC: Post-Quantum Cryptography)の登場

そこで、世界はRSA暗号そのものを諦め、新しい暗号アルゴリズムへの移行を進めています。これが耐量子暗号(PQC)です。
PQCの代表格が「格子暗号(Lattice-based Cryptography)」です。

格子暗号は、数千次元の多次元空間に並んだ「点の格子」をベースにしています。
この格子空間において、「原点に近い最も近い格子点を探す」という問題(最短ベクトル問題:SVP)や、「格子点にわずかなノイズを加えた座標から、元の格子点を当てる」という問題(LWE問題)は、量子コンピュータを用いても効率的に(多項式時間で)解くアルゴリズムが存在しない(NP困難 / 平均的困難とされる)ことが数学的に証明されています。

現在、米国立標準技術研究所(NIST)が主導し、格子暗号をベースにした「ML-KEM(旧Kyber)」や「ML-DSA」といった次世代暗号規格の標準化と社会実装が急速に進められています。


まとめ:Play the Tech(技術を遊ぶ)

非対称鍵暗号の安全性を探求すると、単なるプログラムの最適化にとどまらず、CPUアーキテクチャの実行効率、量子力学を利用した計算のパラダイムシフト、そして高次元幾何学を用いた未来のセキュリティまで、壮大なテクノロジーの連鎖が見えてきます。

「絶対に破れない」とされた壁が崩れ去るとき、人類は新しい数学の道具を持ってさらに強固な砦を築く。これこそが、セキュリティとテクノロジーが織りなす究極の知恵比べであり、工学の持つ美しさそのものです。

私たちが何気なくタップしているスマホ画面の裏側で動く数式たちに、少しでも匠の息吹を感じていただければ幸いです。


付録:検証用プログラムソースコード一覧

本記事のベンチマーク比較およびシミュレーションで実際に使用したプログラムの全ソースコードです。Windows(MinGW-w64 GCC)およびPython3環境でのコンパイル・実行に対応しています。

1. 汎用CPU(スカラー演算)版:rsa_manual_cpu.c

rsa_manual_cpu.c

C
/*
 * rsa_manual_cpu.c
 * 汎用CPU(スカラー演算)版:
 *   1. 試し割り法による素因数分解
 *   2. ミラー・ラビン素数判定
 *   3. 超小型RSAの鍵生成・暗号化・復号デモ
 *   4. 桁数を増やしていったときの分解時間の計測
 *
 * コンパイル (Windows / MinGW-w64, GCC):
 *   gcc -O2 -o rsa_manual_cpu.exe rsa_manual_cpu.c
 *
 * コンパイル (Linux):
 *   gcc -O2 -o rsa_manual_cpu rsa_manual_cpu.c
 */
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <time.h>
/* 128bit中間値が欲しいので __int128 を使う(GCC拡張) */
typedef unsigned long long u64;
typedef unsigned __int128 u128;
/* --- モジュラー演算 --- */
static u64 mulmod(u64 a, u64 b, u64 m) {
    return (u64)(((u128)a * (u128)b) % m);
}
static u64 powmod(u64 base, u64 exp, u64 mod) {
    u64 result = 1;
    base %= mod;
    while (exp > 0) {
        if (exp & 1) result = mulmod(result, base, mod);
        base = mulmod(base, base, mod);
        exp >>= 1;
    }
    return result;
}
/* --- ミラー・ラビン素数判定(決定的:64bit範囲で十分な基底を使用) --- */
static int miller_rabin(u64 n) {
    if (n < 2) return 0;
    if (n % 2 == 0) return n == 2;
    u64 d = n - 1;
    int r = 0;
    while (d % 2 == 0) { d /= 2; r++; }
    /* 64bit全域で決定的に判定できることが知られている基底集合 */
    u64 witnesses[] = {2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37};
    for (size_t i = 0; i < sizeof(witnesses)/sizeof(witnesses[0]); i++) {
        u64 a = witnesses[i];
        if (a >= n) continue;
        u64 x = powmod(a, d, n);
        if (x == 1 || x == n - 1) continue;
        int composite = 1;
        for (int j = 0; j < r - 1; j++) {
            x = mulmod(x, x, n);
            if (x == n - 1) { composite = 0; break; }
        }
        if (composite) return 0;
    }
    return 1;
}
/* --- 拡張ユークリッドの互除法(逆元計算:秘密鍵d導出用) --- */
static long long ext_gcd(long long a, long long b, long long *x, long long *y) {
    if (b == 0) { *x = 1; *y = 0; return a; }
    long long x1, y1;
    long long g = ext_gcd(b, a % b, &x1, &y1);
    *x = y1;
    *y = x1 - (a / b) * y1;
    return g;
}
static u64 modinv(u64 e, u64 phi) {
    long long x, y;
    ext_gcd((long long)e, (long long)phi, &x, &y);
    long long r = x % (long long)phi;
    if (r < 0) r += phi;
    return (u64)r;
}
/* --- 試し割り法:素因数(の一つ)を返す。見つからなければ0 --- */
static u64 trial_division(u64 n) {
    if (n % 2 == 0) return 2;
    for (u64 d = 3; d * d <= n; d += 2) {
        if (n % d == 0) return d;
    }
    return 0; /* n自体が素数 */
}
/* --- 手作業で追いやすい超小型RSAデモ --- */
static void tiny_rsa_demo(void) {
    printf("=== 超小型RSAデモ(暗算追跡用) ===\n");
    u64 p = 61, q = 53;             /* テキスト定番の例 */
    u64 n = p * q;                  /* 3233 */
    u64 phi = (p - 1) * (q - 1);    /* 3120 */
    u64 e = 17;                     /* gcd(e, phi) = 1 を満たす小さい値 */
    u64 d = modinv(e, phi);
    printf("p=%llu, q=%llu, n=p*q=%llu\n", p, q, n);
    printf("phi(n)=(p-1)(q-1)=%llu\n", phi);
    printf("公開鍵 (e, n) = (%llu, %llu)\n", e, n);
    printf("秘密鍵 (d, n) = (%llu, %llu)\n", d, n);
    u64 m = 65; /* 平文(0〜n-1の任意の値) */
    u64 c = powmod(m, e, n);
    u64 m2 = powmod(c, d, n);
    printf("平文 m=%llu -> 暗号文 c=m^e mod n=%llu -> 復号 m'=c^d mod n=%llu\n", m, c, m2);
    printf("復号結果が平文と一致: %s\n\n", (m == m2) ? "成功" : "失敗");
}
/* --- 桁数を増やしたときの素因数分解時間の計測 --- */
static void benchmark_factoring(void) {
    printf("=== 試し割り法による素因数分解ベンチマーク(CPUスカラー版) ===\n");
    printf("%-20s %-20s %-15s\n", "合成数 n (2つの素数の積)", "見つかった因数", "所要時間(秒)");
    /* 桁数を段階的に増やした半素数を用意(p, qは実際に素数判定済み) */
    u64 primes_small[] = {97, 997, 9973, 99991, 999983, 9999991ULL, 99999989ULL};
    int cnt = sizeof(primes_small)/sizeof(primes_small[0]);
    for (int i = 0; i < cnt; i++) {
        u64 p = primes_small[i];
        u64 q = primes_small[i]; /* 近い大きさの2素数の積で半素数を作る(q側は少しずらす) */
        /* qを次に大きい素数に置き換える簡易処理 */
        u64 qq = p + 2;
        while (!miller_rabin(qq)) qq += 2;
        u64 n = p * qq;
        clock_t t0 = clock();
        u64 factor = trial_division(n);
        clock_t t1 = clock();
        double sec = (double)(t1 - t0) / CLOCKS_PER_SEC;
        printf("n=%-18llu factor=%-18llu %.6f\n", n, factor, sec);
    }
    printf("\n");
}
/* 巨大合成数(1回約5秒・2回で10秒負荷)ベンチマーク */
static void benchmark_factoring_20sec(void) {
    u64 p = 4294967291ULL;
    u64 q = 4294967291ULL;
    u64 n = p * q;
    int iterations = 2;
    printf("=== CPUスカラー版 10秒負荷(巨大合成数・2イテレーション) ===\n");
    printf("n = %llu (factor = %llu)\n", n, p);
    printf("イテレーション数: %d\n", iterations);
    printf("実行中...\n");
    clock_t t0 = clock();
    u64 factor = 0;
    for (int i = 0; i < iterations; i++) {
        factor = trial_division(n);
    }
    clock_t t1 = clock();
    double sec = (double)(t1 - t0) / CLOCKS_PER_SEC;
    printf("完了。見つかった因数: %llu\n", factor);
    printf("総所要時間: %.6f 秒 (1イテレーション平均: %.6f 秒)\n\n", sec, sec / iterations);
}
int main(void) {
    tiny_rsa_demo();
    benchmark_factoring();
    benchmark_factoring_20sec();
    return 0;
}

2. AVX2(SIMD)並列高速化版:rsa_manual_avx2.c

rsa_manual_avx2.c

C
/*
 * rsa_manual_avx2.c
 * AVX2版:倍精度浮動小数点(__m256d)で商を算出し、64bit整数ベクトル演算で剰余を
 * 正確に求めるハイブリッド試し割り法。
 * これにより倍精度の精度限界(2^53)を超えた64bit範囲(2^64-1)までの合成数に対して
 * 正確に動作する並列演算を実現します。
 *
 * コンパイル (Windows / MinGW-w64, GCC):
 *   gcc -O2 -mavx2 -o rsa_manual_avx2.exe rsa_manual_avx2.c
 *
 * コンパイル (Linux):
 *   gcc -O2 -mavx2 -o rsa_manual_avx2 rsa_manual_avx2.c
 */
#include <stdio.h>
#include <stdint.h>
#include <time.h>
#include <immintrin.h>
typedef unsigned long long u64;
/* --- スカラー版(比較用) --- */
static u64 trial_division_scalar(u64 n) {
    if (n % 2 == 0) return 2;
    for (u64 d = 3; d * d <= n; d += 2) {
        if (n % d == 0) return d;
    }
    return 0;
}
/* --- AVX2版:4候補同時チェック(高精度64bit整数剰余判定) --- */
static u64 trial_division_avx2(u64 n) {
    if (n % 2 == 0) return 2;
    double dn = (double)n;
    __m256d vn = _mm256_set1_pd(dn);
    __m256i vn_64 = _mm256_set1_epi64x((long long)n);
    __m256i vzero_i = _mm256_setzero_si256();
    for (u64 d = 3; (double)d * (double)d <= dn; d += 8) {
        double arr[4] = { (double)d, (double)(d + 2), (double)(d + 4), (double)(d + 6) };
        __m256d vd   = _mm256_loadu_pd(arr);
        __m256d q    = _mm256_div_pd(vn, vd);
        __m256d qf   = _mm256_floor_pd(q);

        // 32ビット整数に一度変換し、64ビットへ符号拡張(商qfと除数vdは共に32ビット素数以下なので完全に正確)
        __m128i vqf_32 = _mm256_cvttpd_epi32(qf);
        __m256i vqf = _mm256_cvtepi32_epi64(vqf_32);

        __m128i vd_32 = _mm256_cvttpd_epi32(vd);
        __m256i vd_64 = _mm256_cvtepi32_epi64(vd_32);
        // 正確な64ビット乗算(32ビット同士の積のため、_mm256_mul_epu32 で安全かつ操作)
        __m256i vprod = _mm256_mul_epu32(vqf, vd_64);

        // vrem = n - qf * d
        __m256i vrem = _mm256_sub_epi64(vn_64, vprod);

        // 剰余が0であるか比較
        __m256i vcmp = _mm256_cmpeq_epi64(vrem, vzero_i);
        int mask = _mm256_movemask_pd(_mm256_castsi256_pd(vcmp));
        if (mask) {
            for (int i = 0; i < 4; i++) {
                if (mask & (1 << i)) return (u64)arr[i];
            }
        }
    }
    return 0;
}
static void benchmark_20sec(void) {
    u64 p = 4294967291ULL;
    u64 q = p;
    u64 n = p * q;
    int iterations = 2;
    printf("\n=== スカラー版 vs AVX2版 10秒負荷(巨大合成数・2イテレーション) ===\n");
    printf("n = %llu (factor = %llu)\n", n, p);
    printf("実行中...\n");
    clock_t t0 = clock();
    u64 f1 = 0;
    for (int i = 0; i < iterations; i++) {
        f1 = trial_division_scalar(n);
    }
    clock_t t1 = clock();
    u64 f2 = 0;
    for (int i = 0; i < iterations; i++) {
        f2 = trial_division_avx2(n);
    }
    clock_t t2 = clock();
    double sec_scalar = (double)(t1 - t0) / CLOCKS_PER_SEC;
    double sec_avx2   = (double)(t2 - t1) / CLOCKS_PER_SEC;
    double ratio = (sec_avx2 > 0) ? (sec_scalar / sec_avx2) : 0.0;
    printf("スカラー総時間 : %.6f 秒 (平均: %.6f 秒)\n", sec_scalar, sec_scalar / iterations);
    printf("AVX2総時間    : %.6f 秒 (平均: %.6f 秒)\n", sec_avx2, sec_avx2 / iterations);
    printf("速度倍率       : %.2fx\n\n", ratio);
}
int main(void) {
    printf("=== スカラー版 vs AVX2版(4並列・高精度剰余)試し割り法ベンチマーク ===\n");
    printf("(64bit整数の最大範囲で正確に剰余判定可能)\n\n");
    printf("%-20s %-14s %-14s %-10s\n", "n", "scalar(秒)", "AVX2(秒)", "倍率");
    u64 primes[] = {1000003ULL, 10000019ULL, 30000001ULL, 60000011ULL, 90000049ULL};
    int cnt = sizeof(primes)/sizeof(primes[0]);
    for (int i = 0; i < cnt; i++) {
        u64 p = primes[i];
        u64 q = p;
        u64 n = p * q;
        clock_t t0 = clock();
        u64 f1 = trial_division_scalar(n);
        clock_t t1 = clock();
        u64 f2 = trial_division_avx2(n);
        clock_t t2 = clock();
        double sec_scalar = (double)(t1 - t0) / CLOCKS_PER_SEC;
        double sec_avx2   = (double)(t2 - t1) / CLOCKS_PER_SEC;
        double ratio = (sec_avx2 > 0) ? (sec_scalar / sec_avx2) : 0.0;
        printf("n=%-18llu %-14.6f %-14.6f %.2fx  (factor scalar=%llu, avx2=%llu)\n",
               n, sec_scalar, sec_avx2, ratio, f1, f2);
    }
    benchmark_20sec();
    return 0;
}

3. AVX-512(8並列)並列高速化版:rsa_manual_avx512.c

rsa_manual_avx512.c

C
/*
 * rsa_manual_avx512.c
 * AVX-512版:倍精度浮動小数点(__m512d)で商を算出し、64bit整数ベクトル演算で剰余を
 * 正確に求めるハイブリッド試し割り法。
 * これにより倍精度の精度限界(2^53)を超えた64bit範囲(2^64-1)までの合成数に対して
 * 正確に動作する並列演算を実現します。
 *
 * コンパイル (Windows / MinGW-w64, GCC):
 *   gcc -O2 -mavx512f -o rsa_manual_avx512.exe rsa_manual_avx512.c
 */
#include <stdio.h>
#include <stdint.h>
#include <time.h>
#include <immintrin.h>
typedef unsigned long long u64;
/* --- スカラー版(比較用) --- */
static u64 trial_division_scalar(u64 n) {
    if (n % 2 == 0) return 2;
    for (u64 d = 3; d * d <= n; d += 2) {
        if (n % d == 0) return d;
    }
    return 0;
}
/* --- AVX-512版:8候補同時チェック(高精度64bit整数剰余判定) --- */
static u64 trial_division_avx512(u64 n) {
    if (n % 2 == 0) return 2;
    double dn = (double)n;
    __m512d vn = _mm512_set1_pd(dn);
    __m512i vn_64 = _mm512_set1_epi64((long long)n);
    __m512i vzero_i = _mm512_setzero_si512();
    for (u64 d = 3; (double)d * (double)d <= dn; d += 16) {
        double arr[8] = {
            (double)d, (double)(d + 2), (double)(d + 4), (double)(d + 6),
            (double)(d + 8), (double)(d + 10), (double)(d + 12), (double)(d + 14)
        };
        __m512d vd   = _mm512_loadu_pd(arr);
        __m512d q    = _mm512_div_pd(vn, vd);
        __m512d qf   = _mm512_floor_pd(q);

        // 32ビット整数に一度変換し、64ビットへ符号拡張(商qfと除数vdは共に32ビット素数以下なので完全に正確)
        __m256i vqf_32 = _mm512_cvttpd_epi32(qf);
        __m512i vqf = _mm512_cvtepi32_epi64(vqf_32);

        __m256i vd_32 = _mm512_cvttpd_epi32(vd);
        __m512i vd_64 = _mm512_cvtepi32_epi64(vd_32);
        // 正確な64ビット乗算(32ビット同士の積のため、_mm512_mul_epu32 で安全かつ正確)
        __m512i vprod = _mm512_mul_epu32(vqf, vd_64);

        // vrem = n - qf * d
        __m512i vrem = _mm512_sub_epi64(vn_64, vprod);

        // 剰余が0であるか比較(マスク判定)
        __mmask8 cmp = _mm512_cmpeq_epi64_mask(vrem, vzero_i);
        if (cmp) {
            for (int i = 0; i < 8; i++) {
                if (cmp & (1 << i)) return (u64)arr[i];
            }
        }
    }
    return 0;
}
static void benchmark_20sec(void) {
    u64 p = 4294967291ULL;
    u64 q = p;
    u64 n = p * q;
    int iterations = 2;
    printf("\n=== スカラー版 vs AVX-512版 10秒負荷(巨大合成数・2イテレーション) ===\n");
    printf("n = %llu (factor = %llu)\n", n, p);
    printf("実行中...\n");
    clock_t t0 = clock();
    u64 f1 = 0;
    for (int i = 0; i < iterations; i++) {
        f1 = trial_division_scalar(n);
    }
    clock_t t1 = clock();
    u64 f2 = 0;
    for (int i = 0; i < iterations; i++) {
        f2 = trial_division_avx512(n);
    }
    clock_t t2 = clock();
    double sec_scalar = (double)(t1 - t0) / CLOCKS_PER_SEC;
    double sec_avx512 = (double)(t2 - t1) / CLOCKS_PER_SEC;
    double ratio = (sec_avx512 > 0) ? (sec_scalar / sec_avx512) : 0.0;
    printf("スカラー総時間 : %.6f 秒 (平均: %.6f 秒)\n", sec_scalar, sec_scalar / iterations);
    printf("AVX-512総時間  : %.6f 秒 (平均: %.6f 秒)\n", sec_avx512, sec_avx512 / iterations);
    printf("速度倍率       : %.2fx\n\n", ratio);
}
int main(void) {
    printf("=== スカラー版 vs AVX-512版(8並列・高精度剰余)試し割り法ベンチマーク ===\n");
    printf("(64bit整数の最大範囲で正確に剰余判定可能)\n\n");
    printf("%-20s %-14s %-14s %-10s\n", "n", "scalar(秒)", "AVX-512(秒)", "倍率");
    u64 primes[] = {1000003ULL, 10000019ULL, 30000001ULL, 60000011ULL, 90000049ULL};
    int cnt = sizeof(primes)/sizeof(primes[0]);
    for (int i = 0; i < cnt; i++) {
        u64 p = primes[i];
        u64 q = p;
        u64 n = p * q;
        clock_t t0 = clock();
        u64 f1 = trial_division_scalar(n);
        clock_t t1 = clock();
        u64 f2 = trial_division_avx512(n);
        clock_t t2 = clock();
        double sec_scalar = (double)(t1 - t0) / CLOCKS_PER_SEC;
        double sec_avx512 = (double)(t2 - t1) / CLOCKS_PER_SEC;
        double ratio = (sec_avx512 > 0) ? (sec_scalar / sec_avx512) : 0.0;
        printf("n=%-18llu %-14.6f %-14.6f %.2fx  (factor scalar=%llu, avx512=%llu)\n",
               n, sec_scalar, sec_avx512, ratio, f1, f2);
    }
    benchmark_20sec();
    return 0;
}

4. GPU(OpenCL)並列高速化版:rsa_manual_opencl.c

rsa_manual_opencl.c

C
/*
 * rsa_manual_opencl.c
 * OpenCL版:多数のGPUスレッド(ワークアイテム)に候補除数を1つずつ割り当て、
 * 全候補を並列でチェックする試し割り法。
 *
 * コンパイル (Windows / MinGW-w64, GCC。OpenCL SDKのヘッダ・ライブラリが必要):
 *   gcc -O2 -I"<OpenCL_include>" -L"<OpenCL_lib>" -o rsa_manual_opencl.exe rsa_manual_opencl.c -lOpenCL
 *
 * または、カレントディレクトリにCLヘッダーを配置し、システムのDLLを直接リンクする場合:
 *   gcc -O2 -I. -o rsa_manual_opencl.exe rsa_manual_opencl.c C:\Windows\System32\OpenCL.dll
 *
 * コンパイル (Linux):
 *   gcc -O2 -o rsa_manual_opencl rsa_manual_opencl.c -lOpenCL
 *
 * 実行にはGPU(またはCPU上で動くOpenCLランタイム)が必要です。
 * 手元にNVIDIA/AMD/Intelいずれかの環境があれば、そのまま動作します。
 */
#define CL_TARGET_OPENCL_VERSION 120
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <string.h>
#include <time.h>
#include <CL/cl.h>
typedef unsigned long long u64;
/* GPU側カーネル:各ワークアイテムが1つの候補除数dを担当し、n % d == 0 かを判定する
 * メモリ使用量を削減するため、発見した因数を結果バッファのアドレスに直接書き込む */
static const char *kernel_src =
"__kernel void trial_division(__global const ulong *n_ptr,\n"
"                              __global const ulong *base_ptr,\n"
"                              __global ulong *result) {\n"
"    ulong n = *n_ptr;\n"
"    ulong base = *base_ptr;\n"
"    size_t gid = get_global_id(0);\n"
"    ulong d = base + gid * 2; /* 奇数候補のみを走査 */\n"
"    if (d >= 3 && n % d == 0) {\n"
"        *result = d;\n;\n"
"    }\n"
"}\n";
int main(void) {
    cl_int err;
    cl_uint num_platforms = 0;
    clGetPlatformIDs(0, NULL, &num_platforms);
    if (num_platforms == 0) {
        fprintf(stderr, "OpenCLプラットフォームが見つかりません。"
                        "GPUドライバ(またはCPU用OpenCLランタイム)を導入してください。\n");
        return 1;
    }
    cl_platform_id platform;
    clGetPlatformIDs(1, &platform, NULL);
    cl_device_id device;
    err = clGetDeviceIDs(platform, CL_DEVICE_TYPE_GPU, 1, &device, NULL);
    if (err != CL_SUCCESS) {
        /* GPUが無ければCPUデバイスにフォールバック(動作確認用) */
        err = clGetDeviceIDs(platform, CL_DEVICE_TYPE_CPU, 1, &device, NULL);
        if (err != CL_SUCCESS) {
            fprintf(stderr, "使用可能なOpenCLデバイスが見つかりません。\n");
            return 1;
        }
        fprintf(stderr, "[注意] GPUが見つからないため、OpenCL経由のCPUデバイスで実行します。\n");
    }
    cl_context context = clCreateContext(NULL, 1, &device, NULL, NULL, &err);
    cl_command_queue queue = clCreateCommandQueue(context, device, 0, &err);
    cl_program program = clCreateProgramWithSource(context, 1, &kernel_src, NULL, &err);
    err = clBuildProgram(program, 1, &device, NULL, NULL, NULL);
    if (err != CL_SUCCESS) {
        char log[4096];
        clGetProgramBuildInfo(program, device, CL_PROGRAM_BUILD_LOG, sizeof(log), log, NULL);
        fprintf(stderr, "カーネルビルドエラー:\n%s\n", log);
        return 1;
    }
    cl_kernel kernel = clCreateKernel(program, "trial_division", &err);
    /* 対象n(4294967291 * 4294967291 = 18446744030759878681) */
    u64 n = 4294967291ULL * 4294967291ULL;
    u64 base = 3;
    size_t work_items = (size_t)(4294967291ULL / 2); /* 奇数候補数 = 約21億4700万 */
    int iterations = 2;
    cl_mem n_buf = clCreateBuffer(context, CL_MEM_READ_ONLY | CL_MEM_COPY_HOST_PTR, sizeof(u64), &n, &err);
    cl_mem base_buf = clCreateBuffer(context, CL_MEM_READ_ONLY | CL_MEM_COPY_HOST_PTR, sizeof(u64), &base, &err);
    cl_mem result_buf = clCreateBuffer(context, CL_MEM_READ_WRITE, sizeof(u64), NULL, &err);
    clSetKernelArg(kernel, 0, sizeof(cl_mem), &n_buf);
    clSetKernelArg(kernel, 1, sizeof(cl_mem), &base_buf);
    clSetKernelArg(kernel, 2, sizeof(cl_mem), &result_buf);
    printf("=== GPU (OpenCL) 10秒負荷(巨大合成数・2イテレーション) ===\n");
    printf("n = %llu (factor = 4294967291)\n", n);
    printf("ワークアイテム数: %zu\n", work_items);
    printf("実行中...\n");
    struct timespec t0, t1;
    clock_gettime(CLOCK_MONOTONIC, &t0);
    u64 found = 0;
    for (int iter = 0; iter < iterations; iter++) {
        // 結果バッファを 0 にリセット
        u64 zero = 0;
        clEnqueueWriteBuffer(queue, result_buf, CL_TRUE, 0, sizeof(u64), &zero, 0, NULL, NULL);
        err = clEnqueueNDRangeKernel(queue, kernel, 1, NULL, &work_items, NULL, 0, NULL, NULL);
        clFinish(queue);
        // 最後のループのみ結果読み取り
        if (iter == iterations - 1) {
            clEnqueueReadBuffer(queue, result_buf, CL_TRUE, 0, sizeof(u64), &found, 0, NULL, NULL);
        }
    }
    clock_gettime(CLOCK_MONOTONIC, &t1);
    double sec = (t1.tv_sec - t0.tv_sec) + (t1.tv_nsec - t0.tv_nsec) / 1e9;
    printf("完了。見つかった因数: %llu\n", found);
    printf("GPU並列総実行時間: %.6f 秒 (1イテレーション平均: %.6f 秒)\n\n", sec, sec / iterations);
    clReleaseMemObject(n_buf);
    clReleaseMemObject(base_buf);
    clReleaseMemObject(result_buf);
    clReleaseKernel(kernel);
    clReleaseProgram(program);
    clReleaseCommandQueue(queue);
    clReleaseContext(context);
    return 0;
}

5. Shorの周期発見シミュレーション(Python):shor_period_finding_simulation.py

shor_period_finding_simulation.py

C
"""
shor_period_finding_simulation.py
Shorのアルゴリズムの核心(量子フーリエ変換による周期発見)を、
古典コンピュータ上でシミュレーションし、「量子重ね合わせ→測定」で
なぜ一撃で周期rが見えるのかを体感するためのスクリプト。
これは量子コンピュータの代わりに、小さな系(N=15)に限って
状態ベクトルを配列として愚直に計算しているだけであり、
本物の量子コンピュータの指数的な資源優位性そのものを再現するものではない
(配列サイズが2^n bitで指数的に増えるため、大きなNではこの古典シミュレーション
自体が破綻する。それこそが量子コンピュータが必要な理由)。
実行:
    python3 shor_period_finding_simulation.py
Windows環境での実行(日本語文字化け防止):
    $env:PYTHONIOENCODING="utf-8"; python shor_period_finding_simulation.py
    または
    set PYTHONIOENCODING=utf-8 && python shor_period_finding_simulation.py
"""
import numpy as np
def f(a, N, x):
    return pow(a, x, N)
def simulate_period_finding(a, N, Q):
    """
    Q個の位相状態にわたって f(x) = a^x mod N を計算し、
    「周期r ごとに同じ値が現れる」という構造を、
    離散フーリエ変換(QFTの古典版)で可視化する。
    """
    xs = list(range(Q))
    values = np.array([f(a, N, x) for x in xs])
    # f(x)が周期r(余りが最初に1に戻る周期)を持つことを直接確認
    r = None
    for candidate_r in range(1, Q):
        if values[candidate_r] == values[0]:
            r = candidate_r
            break
    # 古典DFT(量子コンピュータならQFTを1回の演算で実行できる部分)
    # values自体ではなく、「f(x)の値が特定の値になっているか」を
    # 示す指標関数(例えばf(x)==1の位置)にDFTをかけると、
    # 周期rに対応する周波数にピークが立つ。
    indicator = (values == values[0]).astype(float)
    spectrum = np.abs(np.fft.fft(indicator))
    peak_freqs = np.argsort(spectrum)[::-1][:6]
    return values, r, spectrum, sorted(peak_freqs.tolist())
if __name__ == "__main__":
    a, N, Q = 7, 15, 32
    values, r, spectrum, peaks = simulate_period_finding(a, N, Q)
    print(f"N = {N} を分解したい。ランダムに選んだ底 a = {a}")
    print(f"f(x) = {a}^x mod {N} を x=0..{Q-1} まで計算:")
    print(values.tolist())
    print(f"\n=> 周期 r = {r} (f(x+r) = f(x) となる最小の正整数)")
    print(f"\n古典DFTスペクトルのピーク周波数(上位6件, Q={Q}): {peaks}")
    print(f"Q/r = {Q}/{r} = {Q/r:.3f}")
    print("=> ピークがQ/rの整数倍の位置に立っていることが確認できる。")
    print("   本物の量子コンピュータでは、この「ピークの位置」を")
    print("   重ね合わせ状態への1回のQFTと測定だけで得られる。")
    print("   古典コンピュータは、これをxごとに逐次計算してからでないと")
    print("   スペクトルを作れない(=全体でO(Q)の逐次計算が必要)。")
    # 古典後処理:周期rからNの素因数を復元する(Shorのアルゴリズムの仕上げ)
    if r % 2 == 0:
        x = pow(a, r // 2, N)
        import math
        p = math.gcd(x - 1, N)
        q = math.gcd(x + 1, N)
        print(f"\n[古典後処理] a^(r/2) mod N = {x}")
        print(f"gcd({x}-1, {N}) = {p}")
        print(f"gcd({x}+1, {N}) = {q}")
        print(f"=> N = {N} = {p} x {q}")

コメント

タイトルとURLをコピーしました