ARM64_Lab

SHA-256 on Windows ARM64: .NET was 8x faster than Python and Node

In this article
  1. Prove the input is identical before timing
  2. SHA-256 alone was in a different league
  3. .NET did not win SHA-1 or SHA-512
  4. I reran it after the median became 7x the minimum
  5. What I did not measure
  6. Change the runtime for large-file hashing

On 2026-08-04, I passed exactly the same 128 MB byte sequence to CPython 3.12.10, Node.js 24.13.0, and .NET 9 for SHA-256. The results were 241.1 MB/s, 289.9 MB/s, and 1,882.0 MB/s.

That was surprising. The CPU was the same Snapdragon X Elite X1E80100, and all three processes were native ARM64. Yet .NET reached 7.8 times Python's speed.

I ran this because the open question from hash-throughput-blake2b, written on 2026-06-10, had been bothering me. In that test, blake2b reached 615 MB/s while sha256 was last at 145 MB/s. ARM64 should have SHA-256 instructions, so why was it so slow? Was the CPU missing cryptographic extensions, or was the implementation responsible? I did not want to leave that ambiguous.

Prove the input is identical before timing

The first requirement was to guarantee that all three runtimes processed exactly the same input. Random data generated separately would differ by runtime, so I implemented xorshift32 in Python, JavaScript, and C#, generated 1 MB, and repeated it 128 times to make 128 MB.

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)

The first 16 digits of the SHA-256 digest from all three runtimes were 885a4d877a6979ff. The input and output matched. I compared the digest on every run and recorded the result as same_input, because a speed discussion is invalid if the inputs do not match. The first version used different random data for each runtime and produced different digests; I nearly discussed the speed gap from that run. That would have been unsafe.

SHA-256 alone was in a different league

Here are the medians across nine rounds and the fastest value. MB/s is 128 MB divided by elapsed time.

Runtime Crypto implementation Median Median MB/s Fastest 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

When I ran it again on another date, the results were 235.0 MB/s, 300.3 MB/s, and 1,939.2 MB/s. The order and ratio were almost unchanged. The .NET/Python gap was 7.8x and 8.3x, settling around 8x on the rerun.

This showed that the CPU can produce 1.9 GB/s for SHA-256. The Snapdragon X Elite is not inherently bad at SHA-256. I had been close to accepting “hashing is slow because this is ARM,” so it was a relief not to blame the CPU for the earlier 145 MB/s result.

.NET did not win SHA-1 or SHA-512

The next question was whether “.NET is simply a faster runtime” explained the result. I measured SHA-1 and SHA-512 with the same three runtimes.

Algorithm 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

For SHA-1, Node reached 783.1 MB/s and beat .NET at 478.6 MB/s. For SHA-512, .NET was last at 343.2 MB/s. If .NET's hash implementation were generally superior, it should lead for SHA-1 and SHA-512 too, but it did not. The pattern made it more likely that the important difference was whether the implementation could use a particular instruction.

.NET was far ahead only for SHA-256. This fits a mixture of implementations where an algorithm-specific hardware instruction is available on one path but not another. ARMv8 cryptographic extensions cover SHA-1 and SHA-256; SHA-512 uses a later, separate extension, so nobody improving on SHA-512 is not inconsistent.

The OpenSSL version difference was also interesting. Node's 3.5.4 was 2.1 times faster than Python's 3.0.16 for SHA-1. On the same CPU, same input, and same native ARM64 execution, that is too large to put on the CPU. Either OpenSSL 3.0.16 takes a different path, or the route to ARM instructions differs.

I reran it after the median became 7x the minimum

The first measurement method failed. I ran nine invocations for each runtime in one consecutive block, and Python SHA-256 ended up with a 11,927.2 ms median, a 1,699.5 ms minimum, and a 33,926.1 ms maximum. The median was seven times the minimum. That could not distinguish a runtime difference from unrelated load during the measurement.

This is an everyday work machine, so the harness needed to spread background load across all three paths. I changed one round to run each runtime once, in round-robin order: Python → Node → .NET.

for r in range(ROUNDS):
    for name, cmd in cmds.items():
        d = _run(cmd)

The data was still noisy. Python SHA-512 ranged from a 324.3 ms minimum to a 9,363.9 ms maximum, and one run's median fell to 50.4 MB/s. I did not trust that column's median and used the fastest value of 394.7 MB/s in the table. The change in which statistic was used is intentional and documented here.

The harness itself also failed once. When dotnet build returned an error in Japanese, subprocess used cp932 by default and stopped with UnicodeDecodeError. The benchmark was not failing; the code reading its output was. Specifying encoding="utf-8" fixed it. This is the kind of trap I encounter every time and still forget.

What I did not measure

I did not change a runtime feature-detection setting such as OPENSSL_armcap, and I did not disassemble the implementations. Therefore I cannot conclude that Python's OpenSSL 3.0.16 is failing to use ARMv8 SHA-256 instructions. The precise statement is only that the speed pattern looks that way.

I also excluded file-I/O comparisons. I planned to include Get-FileHash and certutil, but the separate process that called Get-FileHash could not resolve the cmdlet; it recorded elapsed time with an empty digest. A table with an empty result cannot be used, so I left it out and will measure it again.

I did not include a comparison against x64 emulation. x64-emulation-overhead-2026-08 had shown the emulated side 1.35x faster for SHA-256, but in light of this result it is possible that neither Python path was using cryptographic extensions and that another factor was visible. I did not verify this.

Change the runtime for large-file hashing

For ordinary scripts, I used to call hashlib without thinking. Up to a few dozen megabytes, the difference is not noticeable, and I had not been blocked by it.

For multi-gigabyte duplicate checks, 241.1 MB/s versus 1,882.0 MB/s becomes waiting time. At 10 GB, that is a difference between the 40-second range and the 5-second range, which feels like a different operation. If Python is required and SHA-256 is not mandatory, blake2b is another option; for a checksum, it is often sufficient.

The defensible conclusion is: “SHA-256 on the Snapdragon X Elite is fast, but the runtime matters.” The 1.9 GB/s ceiling is visible now, so the next question is whether Python can approach it. I would need to separate OpenSSL build options from runtime feature detection before claiming to know the cause.

Benchmark .NET Architecture
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.