hashlib の blake2b が 615 MB/s、sha256 が 145 MB/s だった
64MB のランダムデータを hashlib に通したら、blake2b が615 MB/sでいちばん速く、sha256 は145 MB/sでいちばん下に沈んでしまった(この並びは、最初の予想とかなり違った)。
最初に思っていた並びとは逆で、少し嫌な表に見える。実際に測った表を見るまで気づかなかったし、手元では sha256 が上位に来ると思い込んでいた。ARM64 なら sha256 はハードウェア命令で気持ちよく伸びるはず、という雑な期待を持っていたのに、md5 の315 MB/s、sha1 の388 MB/s、sha512 の289 MB/s、sha3_256 の351 MB/sより下にいる。blake2b が速いこと自体は納得できるとしても(64bit寄りの処理が素直に効いた可能性はある)、sha256 がここまで遅いとは考えていなかった。
測定に使った Python は3.12.10で、hashlib の裏側にいる OpenSSL は OpenSSL 3.0.16 11 Feb 2025 だった。この組み合わせを見た瞬間、Pythonの書き方よりも OpenSSL のビルドや実行時検出のほうを疑いたくなる。ただ、逆アセンブルも OpenSSL の設定確認もしていないので、原因を決めるのはまだ早い。2026年6月10日に整理し、元ログの採取時刻は2026-06-10T20:17:43.225432+09:00として残っている。
同じ64MBを何度も噛ませる
やったことは、64MB のランダムなバイト列をひとつ作り、それを md5、sha1、sha256、sha512、blake2b、blake2s、sha3_256 に順番に渡すだけ。各アルゴリズムは5回ずつ測り、中央値から MB/s を出している。
import hashlib
import os
import time
data = os.urandom(64 * 1024 * 1024)
algos = ["md5", "sha1", "sha256", "sha512", "blake2b", "blake2s", "sha3_256"]
for name in algos:
h = getattr(hashlib, name)
runs = []
for _ in range(5):
t0 = time.perf_counter()
h(data).digest()
runs.append((time.perf_counter() - t0) * 1000)
print(name, runs)
ファイル I/O は入れていない。ディスクから読む時間を混ぜると、ハッシュ関数の差が薄まってしまうからで、ここでは同じメモリ上のデータを digest() する時間だけを見たかった。64MB は短すぎてタイマーだけを見る感じでもなく、長すぎて待つほどでもない(このへんのサイズ選びはかなり手癖だった)。
hashlib を選んだのは、普段のスクリプトで一番そのまま使うから。ファイルの重複検出や内容確認では、外部コマンドを呼ぶより Python 標準ライブラリだけで済ませたい場面が多く、同じプロセス内で digest() まで出せるほうが扱いやすい。では標準の範囲で何を選ぶと待ち時間が少ないのか、という素朴な確認だった。
表で見ると sha256 の位置が目立つ
5回分の実測値をそのまま載せる。中央値だけを見ると安定しているように見えるが、sha256 だけは揺れが大きく、ここをならして読んでしまうと引っかかりを見落とす。
| アルゴリズム | runs_ms(5回) | median_ms | MB/s |
|---|---|---|---|
| md5 | 209.51 / 200.11 / 199.68 / 203.04 / 216.26 | 203.04ms | 315 MB/s |
| sha1 | 165.00 / 174.22 / 160.07 / 160.25 / 176.43 | 165.00ms | 388 MB/s |
| sha256 | 291.14 / 441.03 / 479.89 / 283.45 / 474.01 | 441.03ms | 145 MB/s |
| sha512 | 221.59 / 228.63 / 251.96 / 190.54 / 213.93 | 221.59ms | 289 MB/s |
| blake2b | 95.53 / 104.33 / 102.56 / 104.06 / 121.91 | 104.06ms | 615 MB/s |
| blake2s | 161.11 / 152.84 / 165.72 / 176.99 / 177.32 | 165.72ms | 386 MB/s |
| sha3_256 | 162.95 / 182.35 / 191.13 / 159.40 / 199.96 | 182.35ms | 351 MB/s |
blake2b の615 MB/sは、表の中でかなり浮いている。次の sha1 が388 MB/s、blake2s が386 MB/sなので、ひとつだけ別の段にいるように見える。とはいえ、ここからマイクロアーキテクチャの話へ飛ぶには材料が足りず、測ったのは Python から hashlib を呼んだ結果であって、命令単体の性能ではなかった。
気になったのは、やはり sha256 の145 MB/sだった。sha512 は289 MB/sなので、名前の印象だけなら重そうな sha512 のほうがほぼ倍速く見える。sha256 の5回は291.14ms、441.03ms、479.89ms、283.45ms、474.01msで、速い回と遅い回の差も大きい(このばらつきがまた気持ち悪いし、中央値だけにすると見落としそうになる)。
OpenSSL を疑うが、まだ決めない
ARM64 には SHA-256 向けの暗号拡張がある。だから、ネイティブの Python で hashlib.sha256() を呼べば自然に速くなるだろう、と私は思っていた。実際にはそうならず、最初の解釈は失敗してしまった。
まず疑うべきは Python ではなく OpenSSL だろう。hashlib の多くは OpenSSL の実装に乗るし、今回の JSON には OpenSSL 3.0.16 11 Feb 2025 と出ている。古い OpenSSL に落ちている話ではなさそうだが、ARM の暗号拡張を有効にしてビルドされているか、実行時に該当パスを選べているかは別問題(ここを見ていないのが弱いところ)。
ここで「CPU が悪い」と書くのは簡単だが、たぶん雑すぎる。OpenSSL の経路、Python の呼び出し、測定時のスケジューリング、どれも候補には残る。なぜ sha256 だけここまで揺れたのか。答えを出すには、OpenSSL 側の feature 表示や別ビルドでの再測定が要るはずだ。
x64 エミュレーションの記事とは混ぜない
このサイトには、x64 エミュレーションで SHA-256 だけ逆転した記事がある。今回はその比較ではなく、ARM64 ネイティブの Python 3.12.10 の中で、アルゴリズムを差し替えたらどう見えたかという話。
そのため、「x64 のほうが速いか遅いか」は扱わないことにした。今回わかったのは、同じ hashlib の中でも選ぶアルゴリズムで差が出ること、そして sha256 が期待どおりの位置にいなかったことまで。測っていない範囲を混ぜると、数字の見た目だけで話を大きくしてしまう(ARM64、SHA-256、OpenSSL、暗号拡張という言葉が並ぶので、なおさら言いたくなる)。
実用上は、暗号学的な強さを第一にしない重複検出なら blake2b を使ってみることにした。615 MB/sなら、この条件では待ち時間が短いし、Python 標準の hashlib.blake2b() で済む。依存関係も増やさずに済む。
一方で、外部システムとの互換性が必要な場所では sha256 を残す。既存のチェックサム、API、配布物の検証などは、こちらの都合で blake2b に変えられない。145 MB/sでも64MBなら441.03msなので、単発なら困るほどではない、という整理にした。
次にやるなら、OpenSSL のビルド設定と CPU feature の検出結果を見る。可能なら別の OpenSSL ビルドや別の Python 配布でも同じ64MBで試したい。正直、今回のログだけでは「sha256 が最遅だった」までは言えるが、「なぜ最遅だったか」は途中のまま。