ARM64_Lab

AES-GCM が Python 67.6 MB/s、.NET 6143.2 MB/s。同じ CPU で 90 倍離れた

この記事の見出し
  1. 測り方
  2. まず出力が一致することを確かめた
  3. 結果
  4. OpenSSL を新しくしても速くならなかった
  5. 鍵長を変えると .NET だけ理屈どおりに落ちた
  6. CNG の鍵長スケーリングは再現しなかった
  7. .NET が CNG より速い理由は分かっていない
  8. 測っていないこと
  9. 今日わかったこと

同じ 128MB のデータを AES-256-GCM で暗号化させたら、Python の cryptography が 67.6 MB/s、.NET 9 が 6143.2 MB/s だった。90.9 倍である。CPU は同じ Snapdragon X Elite X1E80100、OS も同じ Windows 11 ARM64、暗号文の中身も 1 バイト違わない。それでこの差がついた。

2026年8月6日に書いた python-cng-sha256-ctypes で、宿題を 2 つ残していた。1 つは「cryptography パッケージのように自前で新しい OpenSSL を抱えている経路は試していない」。もう 1 つは「AES をちゃんと測っていない」。今回はその両方を片付けにいった。

測り方

計測スクリプトは scripts/aes_throughput_arm64.py に置いた。xorshift32 で作った 1MB を 128 個並べて 128MB の入力を作り、次の 4 経路に同じものを流す。

  • cryptography 46.0.3(同梱の OpenSSL 3.5.4)
  • pycryptodomex 3.23.0
  • Windows CNG(bcrypt.dll を ctypes で直叩き)
  • .NET 9.0.18(別プロセス、System.Security.Cryptography

Python 側は 1 ラウンドにつき各実装を 1 回ずつ回すラウンドロビンにした。この機械は Defender も OneDrive も動いている普段使いの端末なので、実装ごとにまとめて回すと後半の実装が不利になる。中央値は当てにならず、実際 1 回目の計測では中央値が最小値の半分近くまで落ちた。以下の数字は全部最小値を使っている。

まず出力が一致することを確かめた

速度を比べる前に、4 経路が同じ暗号文を吐いていることを確認した。先頭 8 バイトを 16 進で並べるとこうなる。

アルゴリズム 出力の先頭 8 バイト
AES-128-CBC e8fae9c6dff5ee70
AES-256-CBC ca81bae42aba59bf
AES-256-GCM 46ec5fe89cf24a1b
SHA-256 91e648e63213b952

Python 3 実装と .NET で完全に一致した。言語もランタイムも違うのに同じ値が出たので、少なくとも「片方が手抜きをして速い」ではないと言い切れる。

結果

128MB × 9 ラウンド、単位は MB/s。

cryptography pycryptodome CNG (ctypes) .NET 9
AES-128-CBC 190.6 197.7 696.6 2188.1
AES-256-CBC 134.3 110.0 481.5 1544.1
AES-256-GCM 67.6 41.7 922.7 6143.2
SHA-256 139.0 1424.8 2197.9

GCM の落差がひどい。pycryptodome の 41.7 MB/s は、100MB のファイルを暗号化するのに 2.4 秒かかる速度だ。同じ CPU で .NET は 0.02 秒で終わる。正直、桁を読み間違えたかと思って 2 回見直した。

意外だったのは CBC より GCM のほうが差が開いたことで、思っていたより認証タグの計算が重い。GHASH は PMULL という別の命令に頼る部分なので、AES 命令だけ効いていても追いつかないらしい。

OpenSSL を新しくしても速くならなかった

これが今回いちばん知りたかったところ。hashlib が抱えている OpenSSL は 3.0.16 で、cryptography 46.0.3 が同梱しているのは 3.5.4 と、5 年ぶんくらい世代が違う。しかも両方同じ Python プロセスの中に同居しているので、プロセス起動のブレを挟まずに比べられる。

SHA-256 の結果は次のとおり。

計測 hashlib (OpenSSL 3.0.16) cryptography (OpenSSL 3.5.4)
1 回目 144.0 146.2
2 回目 164.3 139.0

1 回目と 2 回目で順位が入れ替わっている。つまり差はノイズで、OpenSSL を 3.0.16 から 3.5.4 に上げても速くならない。CNG の 1424.8 MB/s との 8 倍以上の開きは、OpenSSL のバージョンでは埋まらなかった。前回の記事では「新しい OpenSSL なら違うかもしれない」と逃げていたが、違わなかった。

鍵長を変えると .NET だけ理屈どおりに落ちた

AES-128 はラウンド数が 10、AES-256 は 14。ハードウェアの AES 命令でラウンドを回している実装なら、CBC のような直列処理では速度が 10/14、つまり 71.4% くらいに落ちるはずだ。

.NET は 2188.1 から 1544.1 に落ちた。比は 70.6% で、理屈とほぼ重なる。中央値どうしで見ても 1912.9 から 1423.6 の 74.4% で、やはり近い。ハードウェア AES がラウンド数ぶんだけ素直に効いている絵になっている。

CNG の鍵長スケーリングは再現しなかった

ここで勘違いをしかけた。1 回目の計測で CNG は 537.5 から 500.8 にしか落ちず、比にすると 93.2% になる。「CNG はラウンド数に縛られていない、何か別のものが律速だ」と結論を書きかけたところで、念のため測り直した。

2 回目は 696.6 から 481.5 で、比は 69.1%。1 回目とまるで違う。再現しない以上、CNG の鍵長スケーリングについては何も言えない。危うく、たまたま揺れた 1 回の数字から理屈をでっち上げるところだった。この機械の CNG 経路は 128MB の AES-128-CBC で 537.5 / 696.6 / 876.1 / 667.2 と大きくばらつく。同じ条件のはずなのにこれだけ動く。

一方 pycryptodome は 1 回目が 54.7%、2 回目が 55.6% と安定して 71.4% より急な落ち方をしていて、こちらはラウンド数だけでは説明できない別の要因がありそうだった。

.NET が CNG より速い理由は分かっていない

腑に落ちないのが .NET と CNG の差だ。Windows 上の .NET の対称暗号は CNG を経由すると理解していたので、ctypes 経由の CNG と大きくは違わないと予想していた。手元では GCM が 6143.2 対 922.7 で、予想と違って 6.6 倍も離れている。

考えられるのは ctypes の呼び出し口とバッファの扱いだが、128MB を 1 回のコールで渡しているので、マーシャリングのコストが 6 倍を説明できるとは思えない。.NET が CNG を経由せず自前の組み込み命令で回している可能性もある。逆アセンブルしていないので、どちらとも決めていない。

測っていないこと

  • .NET が内部で何を呼んでいるか。逆アセンブルも ETW も取っていません
  • CNG の鍵長スケーリング。再現しなかったので結論は保留のまま
  • 小さいペイロード。128MB 一発なので、1KB を 10 万回のような ctypes 呼び出しコストが効く形は別の話になる
  • 復号。CBC の復号は暗号化と違って並列化できるため、傾向が変わる余地がある
  • Go と Rust。そもそもこの機械に入っておらず、比較に入れられませんでした
  • マルチスレッド。ここに並べたのは全部シングルスレッドの数字です

今日わかったこと

Python から AES や SHA-256 をまとまった量流すなら、OpenSSL を新しくしても意味がなく、bcrypt.dll を叩くと 3〜13 倍になる。ただし .NET にはさらに 3〜6 倍離されてしまう。暗号処理が重い部分だけ .NET に切り出す判断は、2026年8月7日時点のこの機械では十分に元が取れる。

数字は Surface Pro 11 / Snapdragon X Elite X1E80100 / Windows 11 ARM64 26200 / Python 3.12.10 / .NET 9.0.18 での実測。

a
arm64lab — 個人運営

Surface Pro 11th Edition(Snapdragon X Elite)を2025年5月から常用機にしている個人の記録です。ARM64 版 Windows で詰まったところと、その場で測った値をそのまま書き残しています。特定の企業・団体とは関係がなく、いかなる組織を代表する見解でもありません。