素数カウントを5処理系で走らせたらJava 8 x64がPython ARM64を大きく抜いた
Java 8 x64 エミュレーションの2630msが、Python 3.12 ARM64 ネイティブの41519msより15倍以上速かった。
今回は「ネイティブかエミュレーションか」より、言語処理系がループをどう実行するかのほうが桁で効いた。2から2,000,000までの整数を試し割りで調べ、素数の個数を数えるだけの単純な計算である。答えは Python 3.12、Node.js 24、Java 8、.NET 9 の4処理系すべてで148933になった。まずここが揃っているので、少なくとも別の問題を解いて速い遅いを比べているわけではない(ここが揃っていないと、速度表はほぼ意味を失う)。
測った日は2026-07-28。この実験ログの日付も2026年7月28日に揃えている。実機は Microsoft Surface Pro, 11th Edition、Snapdragon X 12-core X1E80100 @ 3.40GHz、メモリ32GB、Windows 11 Pro 10.0.26200 ARM64 版だ。電源は AC 接続、電源プランは既定のバランスのまま。特別にベンチマーク用の状態へ寄せず、手元の開発機で実際に走らせた。
想定外だったのは Java 8 で、この環境では x64 エミュレーションで動いているのに、それでも ARM64 ネイティブの Python より明確に速く、エミュレーションという言葉に引っ張られてもっと不利だと思っていた(この時点で、PE アーキだけを見る癖がかなり怪しい気がする)。
同じ試し割りを各処理系へ移す
やったことは単純で、同じアルゴリズムを Python、JavaScript、Java、C# に移植し、それぞれ3回ずつ実行した。各プログラムは標準出力に「素数の個数」と「プログラム内で測った処理時間 ms」を出す。外側ではプロセス起動を含む wall_ms も取っている(ここを分けないと、起動の重さとループ本体が混ざる)。
Python 側は、あえて素の for ループで書いた。
NumPy へ逃がす測定とは別だ。
import time
N = 2_000_000
def is_prime(n):
if n < 2:
return False
d = 2
while d * d <= n:
if n % d == 0:
return False
d += 1
return True
start = time.perf_counter()
count = 0
for i in range(2, N + 1):
if is_prime(i):
count += 1
elapsed = int((time.perf_counter() - start) * 1000)
print(count, elapsed)
C# も同じ形にしている。ライブラリに投げず、分岐と剰余と while だけで押し切る。
using System;
using System.Diagnostics;
const int N = 2_000_000;
static bool IsPrime(int n)
{
if (n < 2) return false;
for (int d = 2; d * d <= n; d++)
{
if (n % d == 0) return false;
}
return true;
}
var sw = Stopwatch.StartNew();
var count = 0;
for (var i = 2; i <= N; i++)
{
if (IsPrime(i)) count++;
}
sw.Stop();
Console.WriteLine($"{count} {sw.ElapsedMilliseconds}");
この2つだけ載せれば十分だと思うし、JavaScript と Java も構造は同じなので、4言語ぶん全部を貼ると記事がコードで埋まる(読み返す側の負担も増える)。
同じ答えでどれだけ時間が違うか
表では、外側で測った wall_ms の3回分、中央値、プログラム内で測った時間、PE アーキを並べた。Java だけ x64、ほかは ARM64 である。
| 処理系 | PE アーキ | wall_ms 3回 | wall_median_ms | プログラム内計測 ms | 出力された個数 |
|---|---|---|---|---|---|
| Python 3.12 | ARM64 | 43282 / 41777 / 41570 | 41777 | 43196 / 41721 / 41519 | 148933 / 148933 / 148933 |
| Node.js 24 | ARM64 | 442 / 425 / 456 | 442 | 319 / 324 / 330 | 148933 / 148933 / 148933 |
| Java 8 | x64 | 2990 / 3196 / 2819 | 2990 | 2734 / 2972 / 2630 | 148933 / 148933 / 148933 |
| .NET 9 | ARM64 | 1997 / 202 / 212 | 212 | 126 / 131 / 134 | 148933 / 148933 / 148933 |
一番速いのは .NET 9 だった。プログラム内計測では134msの回があり、Node.js 24 は330ms、Java 8 は2630ms、Python 3.12 は41519msまでかかった。Python は .NET の310倍という見え方になる。ここまで差が開くと、同じ ARM64 ネイティブという条件はほとんど慰めにならない。
Node.js 24 も速く、330msという数字は Java 8 x64 エミュレーションの2630msよりかなり短い(スクリプト言語というラベルだけでは片づかない)。V8 が単純な数値ループを最適化できる場面では、スクリプト言語という雑な分類だけでは説明できない。
起動込みの時間を見る
今回の JSON には、プログラム内で測った時間と wall_ms の両方がある。差分はおおむねプロセス起動とランタイム初期化のコストになる。
Python 3.12 は1回目が43196msに対して wall が43282msで、差は86ms。2回目は56ms、3回目は51msだった。処理本体が40秒級なので、起動差はほぼ埋もれている、という話。
Node.js 24 は319msに対して442ms、324msに対して425ms、330msに対して456msだった。差は123ms、101ms、126ms。短い処理では無視できないが、今回の順位を変えるほどではない。
Java 8 は2734msに対して2990ms、2972msに対して3196ms、2630msに対して2819ms。差は256ms、224ms、189msで、起動と JVM 初期化の重さが見える。x64 エミュレーション上でも本体のループは Python より速いが、短命プロセスとして何度も呼ぶならこの差は積み上がる。
面食らったのは .NET 9 だ。1回目は126msの計算に対して wall が1997msまで伸びた。差は1871msで、ここだけ見ると起動込みでは Java 8 に近づく。2回目と3回目は202ms、212msに落ち着いていて、差は71ms、78msになった。中央値が212msなので表の順位は変わらないが、初回だけを切り取ると印象が変わる(常駐させるか、毎回起動するかで見え方が変わりそうだ)。
PowerShell は同じ表に入れられなかった
失敗もあった。PowerShell 7 と Windows PowerShell 5.1 も同じ土俵に乗せようとしたが、N=2,000,000では遅すぎて終わらずに止まる。そこで N=200,000 に落として測った。
JSON の note にも、pwsh と powershell は N=200000、ほかは N=2000000 と残っている。条件を揃えられなかったので、上の比較表には入れていなかった。pwsh が643msと493ms、Windows PowerShell が344msと316msで終わっているが、これは小さい N の別競技という扱いにした。
さらに PowerShell の測定では別のバグも踏んでいる。出力が0になっていて、素数カウントとしては明らかにおかしい。ここで無理に解釈すると話が濁るので、PowerShell 側は別記事に回す。今回の記事では、条件を揃えられなかったことだけを記録しておく。
どう受け止めるか
この結果から、Python が数値計算に向かないと雑に切るつもりはなかった。Python は、計算を NumPy やライブラリ側へ渡したときに強い。逆に言うと、今回のようにインタプリタのループを素で回すと、剰余と分岐だけの処理でも41519msまで膨らむ。
個人的には、ARM64 Windows で何かを測るときの見方が少し変わった。最初に見るべきなのは PE アーキだけではなく、なぜそのループが速くなるのかを分けて見る必要がある。ARM64 ネイティブか x64 エミュレーションかはもちろん確認するが、その上で JIT が効くのか、ループがインタプリタに残るのか、プロセス起動が支配的なのかを分けて見る必要がある。
今回の verdict は「動いた」。4処理系で答えが148933に揃い、.NET 9、Node.js 24、Java 8、Python 3.12 の順に差も出た。次に試すなら、同じ素数カウントを NumPy、PyPy、ARM64 版の新しい Java で測り直してみる。そこで初めて、Python という言語名ではなく、どの実行方式に仕事を渡したのかを比べられる。