ARM64_Lab

素数カウントを5処理系で走らせたらJava 8 x64がPython ARM64を大きく抜いた

この記事の見出し
  1. 同じ試し割りを各処理系へ移す
  2. 同じ答えでどれだけ時間が違うか
  3. 起動込みの時間を見る
  4. PowerShell は同じ表に入れられなかった
  5. どう受け止めるか

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 という言語名ではなく、どの実行方式に仕事を渡したのかを比べられる。

a
arm64lab — 個人運営

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