ARM64_Lab

AES-GCM on Snapdragon X Elite: Python 67.6 MB/s vs .NET 6143.2 MB/s

In this article
  1. How I measured it
  2. First, I verified that the outputs matched
  3. Results
  4. A newer OpenSSL did not make it faster
  5. Changing the key length made only .NET slow down as theory predicted
  6. CNG's key-length scaling did not reproduce
  7. I do not know why .NET is faster than CNG
  8. What I did not measure
  9. What I learned today

Encrypting the same 128 MB with AES-256-GCM produced 67.6 MB/s with Python's cryptography and 6143.2 MB/s with .NET 9. That is a 90.9x difference. The CPU was the same Snapdragon X Elite X1E80100, the OS was the same Windows 11 ARM64, and the ciphertext did not differ by a single byte. Yet the performance gap was that large.

On August 6, 2026, python-cng-sha256-ctypes left me with two follow-up questions. One was that I had not tested a path that bundles a newer OpenSSL, like the cryptography package. The other was that I had not properly measured AES. This time I addressed both.

How I measured it

The measurement script is in scripts/aes_throughput_arm64.py. I created a 128 MB input by arranging 128 chunks of 1 MB generated with xorshift32, then passed the same input through these four paths:

  • cryptography 46.0.3 (bundled OpenSSL 3.5.4)
  • pycryptodomex 3.23.0
  • Windows CNG (calling bcrypt.dll directly through ctypes)
  • .NET 9.0.18 (a separate process, System.Security.Cryptography)

On the Python side, each implementation ran once per round in round-robin order. Defender and OneDrive were running on this everyday machine, so running each implementation in one block would disadvantage the implementations measured later. The median was not reliable: in the first measurement, it fell to almost half the minimum. All numbers below therefore use the minimum.

First, I verified that the outputs matched

Before comparing speed, I confirmed that all four paths produced the same ciphertext. The first 8 bytes in hexadecimal were:

Algorithm First 8 output bytes
AES-128-CBC e8fae9c6dff5ee70
AES-256-CBC ca81bae42aba59bf
AES-256-GCM 46ec5fe89cf24a1b
SHA-256 91e648e63213b952

The Python 3 implementations and .NET matched exactly. Even though the languages and runtimes differed, they produced the same values, so at least it is safe to say that one side was not simply faster because it did less work.

Results

128 MB x 9 rounds, in MB/s.

cryptography pycryptodome CNG (ctypes) .NET 9
AES-128-CBC 190.6 197.7 696.6 2188.1
AES-256-CBC 134.3 110.0 481.5 1544.1
AES-256-GCM 67.6 41.7 922.7 6143.2
SHA-256 139.0 1424.8 2197.9

The GCM gap was severe. At pycryptodome's 41.7 MB/s, encrypting a 100 MB file takes 2.4 seconds. .NET finished on the same CPU in 0.02 seconds. I honestly thought I had misread the order of magnitude and checked twice.

What surprised me was that the gap was wider for GCM than for CBC. Computing the authentication tag was more expensive than I expected. GHASH relies partly on a separate PMULL instruction, so having AES instructions alone does not seem to be enough to catch up.

A newer OpenSSL did not make it faster

This was the result I most wanted. The OpenSSL used by hashlib is 3.0.16, while cryptography 46.0.3 bundles 3.5.4, roughly five years apart in generation. Both are also loaded in the same Python process, so the comparison does not include process-start variation.

The SHA-256 results were:

Measurement hashlib (OpenSSL 3.0.16) cryptography (OpenSSL 3.5.4)
Round 1 144.0 146.2
Round 2 164.3 139.0

The ranking reverses between the first and second rounds. In other words, the difference is noise: upgrading OpenSSL from 3.0.16 to 3.5.4 did not make it faster. The more-than-8x gap to CNG at 1424.8 MB/s was not closed by the OpenSSL version. In the previous article I had left myself an escape hatch by saying that a newer OpenSSL might be different. It was not.

Changing the key length made only .NET slow down as theory predicted

AES-128 has 10 rounds and AES-256 has 14. If an implementation uses hardware AES instructions for those rounds, a serial workload such as CBC should fall to about 10/14, or 71.4%, of its speed.

.NET fell from 2188.1 to 1544.1, a ratio of 70.6%, almost exactly matching the theory. Looking at the medians also gave 74.4%, from 1912.9 to 1423.6, again close. The result looks like hardware AES responding directly to the number of rounds.

CNG's key-length scaling did not reproduce

I almost drew the wrong conclusion here. In the first measurement, CNG fell only from 537.5 to 500.8, a ratio of 93.2%. I was about to conclude that CNG was not constrained by the number of rounds and that something else was the bottleneck, so I measured it again.

The second run went from 696.6 to 481.5, a ratio of 69.1%, completely different from the first. Because it did not reproduce, I cannot say anything about CNG's key-length scaling. I nearly built a theory from one number that happened to fluctuate. On this machine, the CNG path's AES-128-CBC throughput for 128 MB varied widely: 537.5 / 696.6 / 876.1 / 667.2, despite supposedly identical conditions.

By contrast, pycryptodome dropped by 54.7% in the first run and 55.6% in the second. Its decline was consistently steeper than 71.4%, suggesting another factor that the round count alone does not explain.

I do not know why .NET is faster than CNG

The difference between .NET and CNG does not make sense to me yet. I understood that .NET symmetric cryptography on Windows goes through CNG, so I expected the ctypes CNG path not to differ greatly. On this machine, GCM was 6143.2 versus 922.7, a 6.6x gap that was not what I expected.

The ctypes call boundary and buffer handling are possible explanations, but the 128 MB input is passed in one call, so I do not think marshaling cost can explain a 6x difference. .NET may also be using built-in instructions without going through CNG. I have not disassembled it, so I am not choosing between those explanations.

What I did not measure

  • What .NET calls internally. I did not disassemble it or collect ETW.
  • CNG key-length scaling. It did not reproduce, so the conclusion remains on hold.
  • Small payloads. This test sends 128 MB at once; a workload such as 100,000 calls with 1 KB each is a different question where ctypes call overhead matters.
  • Decryption. CBC decryption can be parallelized unlike encryption, so the trend could change.
  • Go and Rust. They were not installed on this machine, so I could not include them.
  • Multithreading. All numbers here are single-threaded.

What I learned today

For sending substantial amounts of AES or SHA-256 data from Python, upgrading OpenSSL does not help, while calling bcrypt.dll makes it 3-13x faster. .NET is still another 3-6x ahead. On this machine as of August 7, 2026, moving the crypto-heavy part into .NET can be worthwhile.

These are measurements on a Surface Pro 11 / Snapdragon X Elite X1E80100 / Windows 11 ARM64 26200 / Python 3.12.10 / .NET 9.0.18.

Benchmark Python .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.