SHA-256 on Windows ARM64: .NET was 8x faster than Python and Node
In this article
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.