同じ128MBのSHA-256で8倍差 — 遅いのはSnapdragonではなくOpenSSL 3.0.16だった
この記事の見出し
2026年8月4日、まったく同じ128MBのバイト列を CPython 3.12.10、Node.js 24.13.0、.NET 9 の3つに渡して SHA-256 を計算させたら、241.1 MB/s、289.9 MB/s、1882.0 MB/sという並びになった。
CPU は同じ Snapdragon X Elite X1E80100 で、3つともプロセスは ARM64 ネイティブ。それでも .NET は Python の7.8倍出ている。
この測定をやったのは、2026年6月10日に書いた hash-throughput-blake2b の宿題が残っていたからだ。あのときは blake2b が615 MB/s で、sha256 が145 MB/s と最下位に沈んだ。ARM64 には SHA-256 用の命令があるはずなのに、なぜ sha256 だけ遅いのか。CPU 側に暗号拡張が無いのか、それとも処理系の実装の問題なのか、そこを切り分けないまま置いてあった。
同じバイト列だと証明してから測る
比較の前提として、3処理系がまったく同じ入力を噛んでいることを保証したかった。乱数で作ると処理系ごとに中身が変わるので、xorshift32 を Python と JavaScript と C# にそれぞれ書き、1MB 分を生成してから128回並べて128MBにしている。
x = 20260804
unit = bytearray(1 << 20)
for i in range(len(unit)):
x ^= (x << 13) & 0xffffffff
x ^= x >> 17
x ^= (x << 5) & 0xffffffff
x &= 0xffffffff
unit[i] = x & 0xff
blob = bytes(unit) * (SIZE >> 20)
3処理系が返した SHA-256 の先頭16桁は、どれも 885a4d877a6979ff で一致した。入力が同じで、出力も同じ。ここが揃っていないと速度差の議論そのものが成り立たないので、測定のたびに digest を突き合わせて same_input として記録している(最初の版では処理系ごとに別の乱数を使っていて、digest が食い違っていた)。
SHA-256だけ桁が違う
9ラウンド回した中央値と、その中の最速値を並べる。MB/s は128MBを所要時間で割った値。
| 処理系 | 暗号実装 | 中央値 | 中央値のMB/s | 最速のMB/s |
|---|---|---|---|---|
| CPython 3.12.10 | OpenSSL 3.0.16 | 530.9ms | 241.1 MB/s | 265.6 MB/s |
| Node.js 24.13.0 | OpenSSL 3.5.4 | 441.6ms | 289.9 MB/s | 326.0 MB/s |
| .NET 9.0.18 | System.Security.Cryptography | 68.0ms | 1882.0 MB/s | 2070.7 MB/s |
別の日時にもう一度まわしたら、235.0 MB/s、300.3 MB/s、1939.2 MB/sになった。順位も比率もほぼ動いていない。.NET と Python の差は7.8倍と8.3倍で、測り直しても8倍前後に落ち着く。
つまり、この CPU で SHA-256 が1.9 GB/s 出ること自体は分かった。Snapdragon X Elite が SHA-256 を苦手にしているわけではない。手元では「ARM だからハッシュが遅いのだろう」と雑に納得しかけていたので、ここは予想と違う(あの145 MB/s を CPU のせいにしなくてよかった)。
SHA-1とSHA-512では.NETが勝たない
ここからが本題で、.NET が単に速い処理系だから勝った という説明が成り立つかを確かめたい。同じ3処理系で SHA-1 と SHA-512 も測ってある。
| アルゴリズム | CPython | Node.js | .NET |
|---|---|---|---|
| SHA-256 | 241.1 MB/s | 289.9 MB/s | 1882.0 MB/s |
| SHA-1 | 368.1 MB/s | 783.1 MB/s | 478.6 MB/s |
| SHA-512 | 394.7 MB/s | 432.3 MB/s | 343.2 MB/s |
SHA-1 では Node が783.1 MB/s で、.NET の478.6 MB/s を上回った。SHA-512 にいたっては .NET が343.2 MB/s で3つの中の最下位になっている。もし .NET のハッシュ実装が全般的に優秀なら、SHA-1 でも SHA-512 でも先頭にいるはずだが、そうなっていない。
.NET が桁違いに速いのは SHA-256 のときだけだった。この形は「アルゴリズム専用のハードウェア命令を使えている実装と、使えていない実装が混ざっている」と考えるときれいに揃う。ARMv8 の暗号拡張は SHA-1 と SHA-256 を対象にしていて、SHA-512 用の命令は別の後発拡張なので、SHA-512 で誰も伸びていないのも矛盾しない。
同じ OpenSSL でもバージョン差が出ているのが面白いところで、Node が抱える3.5.4 は、Python が抱える3.0.16 に対して SHA-1 で2.1倍の開きをつけた。同じ CPU、同じ入力、同じ ARM64 ネイティブでこれだけ動くのだから、遅さの原因を CPU に置くのは無理がある。
中央値がminの7倍になって測り直した
最初の測り方は失敗だった。処理系ごとに9回まとめて連続実行する形にしていたら、Python の SHA-256 が中央値11927.2ms、最小1699.5ms、最大33926.1msという、まともに読めない分布になってしまった。中央値が最小の7倍で、これでは処理系の差なのか、測っている最中に別の負荷が乗っただけなのか区別がつかない。
このマシンは実際の作業機なので、裏で何かが走る前提で組むべきだった。直し方としては、1ラウンドを「各処理系1回ずつ」にして、python、node、dotnet の順で回すラウンドロビンに変えている(背景負荷を3者へ均等にばらすのが狙い)。こうすると、特定の処理系だけが不当に重く見える形にはなりにくい。
for r in range(ROUNDS):
for name, cmd in cmds.items():
d = _run(cmd)
それでも汚れは残っていて、Python の SHA-512 は最小324.3msに対して最大9363.9msまで伸び、中央値が50.4 MB/s まで落ちた回がある。この列だけは中央値を信用せず、上の表では最速値の394.7 MB/s を採った。数字を採る位置を変えたことは書いておきたい。
途中で harness 自体も一度落ちている。dotnet build が日本語でエラーを返したとき、subprocess の既定が cp932 で読もうとして UnicodeDecodeError で止まった。測定の本体ではなく、出力を読む側で詰まったので、encoding="utf-8" を明示して直した。
測っていないこと
OPENSSL_armcap のような実行時の機能検出を触っていないし、逆アセンブルもしていない。だから「Python 側の OpenSSL 3.0.16 が ARMv8 の SHA-256 命令を使えていない」と断定するには材料が足りず、速度の形からそう見える、という以上のことは、この測定から言えることではない。
ファイル I/O 込みの比較も外した。Get-FileHash と certutil も並べるつもりで書いたのだが、Get-FileHash を別プロセスで呼ぶところで cmdlet が解決できず、digest が空のまま時間だけ記録されている。空の結果に時間がついている表は捨てるしかなく、この記事では触れないことにした。ここは次に測り直す。
x64 エミュレーション側との比較も今回は入れていない。x64-emulation-overhead-2026-08 で SHA-256 だけエミュレーション側が1.35倍速いという逆転が出ていたが、今回の結果を踏まえると、あれは「どちらの Python も暗号拡張を使えていないので、別の要因が見えていた」可能性がある。これも確かめていない。
大きなファイルを数える用事なら処理系を変える
正直なところ、普段のスクリプトでハッシュを取るときは何も考えずに hashlib を呼んでいた。数十MBまでなら気づかない差だし、実際これまで困っていない。
ただ、数GB単位のファイルを重複判定にかけるような用事だと、241.1 MB/s と1882.0 MB/s の違いはそのまま待ち時間になる。10GB なら40秒台と5秒台の差で、これは体感として別物だ。Python から離れられない場面でも、SHA-256 にこだわる理由がないなら blake2b という逃げ道はある(用途がチェックサムなら、そちらで足りることが多い)。
この記事で言えるのは、Snapdragon X Elite の SHA-256 は速い。ただし処理系を選ぶ というところまで。1.9 GB/s という上限が見えたので、次は Python 側でその値に近づける方法があるのかを見たい。OpenSSL のビルドオプションと実行時検出のどちらが効いているのかを分けないと、原因を当てたことにはならない。