ARM64_Lab

Prime-counting benchmark on Windows on ARM: Java 8 x64 beat native Python by more than 15x

In this article
  1. Porting the same trial division to each runtime
  2. How different were the times for the same answer?
  3. Looking at startup-inclusive time
  4. PowerShell could not be put in the same table
  5. How should this result be read?

Java 8 running under x64 emulation finished in 2,630 ms, more than 15 times faster than native ARM64 Python 3.12, which took 41,519 ms.

This time, how the runtime executes the loop mattered by orders of magnitude more than whether it was native or emulated. The task was simply to test integers from 2 through 2,000,000 by trial division and count the primes. Python 3.12, Node.js 24, Java 8, and .NET 9 all returned 148933. Since that result matched, the comparison was at least measuring the same problem rather than different problems (without that agreement, the timing table would have little meaning).

The measurements were taken on 2026-07-28, and the experiment log is dated 2026-07-28 as well. The machine was a Microsoft Surface Pro, 11th Edition, with a Snapdragon X 12-core X1E80100 @ 3.40GHz, 32GB of memory, and Windows 11 Pro 10.0.26200 ARM64. It was connected to AC power with the default Balanced power plan. I ran it on my everyday development machine rather than preparing a special benchmark environment.

Java 8 was the unexpected result. It ran under x64 emulation on this machine but was still clearly faster than native ARM64 Python. I had assumed that the word “emulation” meant it would be at a greater disadvantage (which made me question how useful it is to look only at the PE architecture).

Porting the same trial division to each runtime

The test was straightforward: I ported the same algorithm to Python, JavaScript, Java, and C#, and ran each three times. Each program printed the prime count and the processing time measured inside the program in milliseconds. Outside the process, I also measured wall_ms, including process startup (without separating these, startup overhead and the loop body would be mixed together).

On the Python side, I deliberately used a plain for loop.

This was not a measurement that moved the work into 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# used the same shape. It did not hand the work to a library; it relied on branches, remainder, and while/for loops.

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}");

The JavaScript and Java programs had the same structure, so I did not include all four implementations and fill the article with code (that would also make it harder to read).

How different were the times for the same answer?

The table contains the three outer wall_ms measurements, their median, the time measured inside the program, and the PE architecture. Java was the only x64 runtime; the others were ARM64.

Runtime PE architecture 3 wall_ms runs wall_median_ms In-program time ms Printed count
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 was the fastest. Its in-program timings included a 134 ms run; Node.js 24 took 330 ms, Java 8 took 2,630 ms, and Python 3.12 took as long as 41,519 ms. Python therefore looks 310 times slower than .NET in this table. At that scale, the fact that several runtimes were native ARM64 is little consolation.

Node.js 24 was also fast. Its 330 ms result was much shorter than Java 8 x64 emulation at 2,630 ms (the label “scripting language” is not enough to explain it). When V8 can optimize a simple numeric loop, that broad label does not describe the result very well.

Looking at startup-inclusive time

The JSON from this run contains both the in-program time and wall_ms. Their difference is roughly the cost of process startup and runtime initialization.

For Python 3.12, the first in-program result was 43,196 ms and wall_ms was 43,282 ms, a difference of 86 ms. The differences on the second and third runs were 56 ms and 51 ms. The loop itself took around 40 seconds, so startup overhead was almost invisible.

Node.js 24 took 319 ms, 324 ms, and 330 ms inside the program, compared with wall times of 442 ms, 425 ms, and 456 ms. The differences were 123 ms, 101 ms, and 126 ms. That is not negligible for a short task, but it was not large enough to change this ranking.

Java 8 took 2,734 ms, 2,972 ms, and 2,630 ms inside the program, compared with 2,990 ms, 3,196 ms, and 2,819 ms wall time. The differences were 256 ms, 224 ms, and 189 ms, showing the cost of startup and JVM initialization. Even under x64 emulation, the loop itself was faster than Python, but repeated short-lived processes would accumulate that overhead.

.NET 9 was the surprising case. The first run had a 126 ms calculation but a wall time as high as 1,997 ms. The difference was 1,871 ms, which makes it look close to Java 8 if only startup-inclusive first-run time is considered. The second and third runs settled at 202 ms and 212 ms, with differences of 71 ms and 78 ms. The 212 ms median did not change the table's ranking, but the first run gives a different impression (whether a process is kept resident or started every time clearly matters).

PowerShell could not be put in the same table

There was also a failure. I tried to put PowerShell 7 and Windows PowerShell 5.1 on the same footing, but at N=2,000,000 they were too slow and did not finish. I therefore lowered N to 200,000.

The JSON note also records that pwsh and powershell used N=200000, while the other runtimes used N=2000000. Since the conditions were not equal, I left them out of the comparison table above. pwsh finished at 643 ms and 493 ms, and Windows PowerShell finished at 344 ms and 316 ms, but I treated those as a separate contest with a smaller N.

The PowerShell measurement had another bug: the output was 0, which is obviously wrong for a prime count. Rather than force an interpretation and muddy the article, I am leaving the PowerShell side for a separate article. This article records only that the conditions could not be aligned.

How should this result be read?

I did not intend to dismiss Python as unsuitable for numeric work. Python is strong when the work is handed to NumPy or another library. Conversely, when an interpreter loop is run directly as in this test, even a task consisting only of remainder operations and branches can grow to 41,519 ms.

This changed how I look at measurements on Windows on ARM64. The first question cannot be only the PE architecture; I also need to separate why a particular loop is fast. I still check whether the executable is native ARM64 or x64 emulation, but then I separate whether a JIT is effective, whether the loop remains in an interpreter, and whether process startup dominates.

The verdict for this article is “worked.” Four runtimes agreed on 148933, and the timings separated .NET 9, Node.js 24, Java 8, and Python 3.12. If I continue this experiment, I would rerun the same prime count with NumPy, PyPy, and a newer ARM64 Java build. Only then could I compare which execution method received the work rather than treating “Python” as the whole explanation.

Benchmark Python Node.js .NET
a
arm64lab — Independent publisher

Personal test notes from a Surface Pro 11th Edition with Snapdragon X Elite, used as a daily machine since May 2025. Results are based on direct measurements and do not represent any company or organization.