ARM64_Lab

Pinning one benchmark to each of 12 cores showed an 80% gap - but not core silicon variance

In this article
  1. Verify the pin before every measurement
  2. The first result followed core number neatly
  3. I counted what was getting in the way
  4. I repeated the test on idle cores
  5. What was measured and what was not
  6. How I would use this

Pinning the same integer loop to each of the 12 cores on a Snapdragon X Elite X1E80100 produced minimum times from 0.1369 to 0.2469 seconds, an 80.4% spread. At first I wondered whether the cores differed in quality, but following the evidence led somewhere else. Honestly, manufacturing variation was the first thing I suspected.

When I read Win32_Processor on 2026-08-02, the machine appeared to have 12 cores and 12 logical processors, with 36864 KB of L2 and 0 L3. The count was clear, but that did not tell me whether all 12 ran at the same speed. When I assigned worker counts with ProcessPoolExecutor on 2026-06-02, I also finished without separating scheduler behavior from core behavior as the reason 8 and 12 workers hit a ceiling. Pinning one core at a time should make that distinction clearer.

Verify the pin before every measurement

With the Windows scheduler in control, I cannot choose the core myself. I used psutil's cpu_affinity to bind the process to one core before measuring. Setting the affinity does not prove the process actually ran there, so I read GetCurrentProcessorNumber before and after each measurement and counted only runs that used the requested core.

python scripts/core_affinity_bench.py --rounds 30 --json .bench/core_affinity_30.json

The measurement date was 2026-08-05. The hardware was a Surface Pro 11th Edition with a Snapdragon X Elite X1E80100, running Windows 11 Pro 26200 ARM64. Python was the 3.12.10 ARM64 build and psutil was 7.2.2. Each run performed 1200000 integer-loop iterations without touching an array. I shuffled the core order every round and ran 30 rounds to remove ordering advantage.

This is my everyday machine, and Defender and resident services remained enabled during the test. Taking an average would let interrupted runs dominate, so I used the minimum for each core as its speed when it was not interrupted.

The first result followed core number neatly

CPU Minimum Median Maximum Ratio to fastest
0 0.2469 s 0.4317 s 0.7626 s 1.804
1 0.2257 s 0.3637 s 0.6324 s 1.649
2 0.1873 s 0.3507 s 0.4375 s 1.368
3 0.1799 s 0.3290 s 0.8579 s 1.314
4 0.1784 s 0.3122 s 0.5486 s 1.303
5 0.1694 s 0.2652 s 0.7408 s 1.238
6 0.1541 s 0.2450 s 0.4014 s 1.126
7 0.1369 s 0.2305 s 0.3747 s 1.000
8 0.2447 s 0.3211 s 0.4104 s 1.788
9 0.1629 s 0.3043 s 0.3966 s 1.190
10 0.1746 s 0.2745 s 0.3430 s 1.276
11 0.1414 s 0.2585 s 0.3246 s 1.033

Zero of the 360 runs failed to stay on the requested core; they all ran where instructed. That part does not appear suspicious.

The point that stopped me was the staircase from cpu0 through cpu7. If this were silicon variation, I would expect fast and slow cores to be scattered without regard to number. A sequence falling from 0.2469 seconds to 0.2257 and then 0.1873 looked more like another force at work than manufacturing variance.

Also, only 22 of 360 runs landed within 10% of the minimum. Almost every run was interrupted by something, making it unreasonable to interpret the result as a pure performance difference.

I counted what was getting in the way

I therefore left my own workload idle and recorded only per-core utilization for 120 seconds. I split it into two 60-second windows to see whether the bias depended on the time window or was persistent.

python scripts/core_load_profile.py --seconds 120 --json .bench/core_load_profile.json
CPU Window 1 Window 2 Overall
0 100.0% 100.0% 100.0%
1 100.0% 100.0% 100.0%
2 100.0% 100.0% 100.0%
3 100.0% 100.0% 100.0%
4 74.3% 70.7% 72.5%
5 60.7% 57.3% 59.0%
6 42.1% 33.7% 37.9%
7 25.5% 18.3% 21.9%
8 17.7% 9.4% 13.5%
9 9.6% 4.9% 7.2%
10 5.9% 2.5% 4.2%
11 2.9% 1.1% 2.0%

It formed a clean staircase. cpu0 through cpu3 were full at 100%, while cpu11 was only 2.0% utilized. The overall average was 51.5%, so an even distribution would have put every core near 50%. Instead, the work was placed toward the front. Ordering by load gave 0,1,2,3,4,5,6,7,8,9,10,11 in both windows, with the rankings perfectly matching. This was not a coincidence; the bias consistently pointed the same way.

The busiest processes were MsMpEng.exe at 118.5%, System at 59.2%, dwm.exe at 53.1%, and SearchIndexer.exe at 41.0%. I wondered whether one of them was restricted to particular cores, but within the range where I could read affinity, all four were allowed on all 12 cores. In other words, no process was selecting cpu0; Windows was placing work there.

The correlation between background load and the measurements reached 0.701 for the median. The correlation with the minimum was 0.459, affected by cpu8 as an outlier.

I repeated the test on idle cores

cpu8 was the problem case. Its load was only 13.5%, but its minimum was 0.2447 seconds, almost as slow as cpu0. In 30 rounds, perhaps I simply never hit an undisturbed window. To check, I narrowed the test to the relatively idle cpu8 through cpu11 and ran 60 rounds.

python scripts/core_affinity_bench.py --cpus 8,9,10,11 --rounds 60
CPU Minimum Median Ratio to fastest
8 0.1098 s 0.3104 s 1.000
9 0.1135 s 0.3071 s 1.033
10 0.1139 s 0.2782 s 1.037
11 0.1169 s 0.2979 s 1.065

The gap narrowed to 6.5%. cpu8's minimum fell from 0.2447 seconds to 0.1098 seconds, making it the fastest core in the comparison. The same core on the same chip became more than twice as fast just by increasing the number of rounds. Calling cpu8 a "slow core" from the first table was my mistake. At 1200000 iterations in 0.1098 seconds, that works out to 10.93 million iterations per second.

What was measured and what was not

On this machine, I can only say that the four idle cores reached within 6.5% of one another when undisturbed. I cannot say anything about cpu0 through cpu3. They remained 100% full even after 30 rounds, so I never reached an undisturbed window. I have not separated whether those four are slow or whether they simply never got a turn.

I also could not verify the claim that two cores reach a higher clock. This measurement showed no sign that exactly two cores were faster. Absence of a signal is not proof of absence, so I keep those statements separate.

Cross-table comparisons need care too. cpu7's 0.1369 seconds in the 30-round table and cpu8's 0.1098 seconds in the 60-round table were measured at different times and under different loads. It is not valid to infer that cpu8 is the faster core. Comparisons are only within each table.

How I would use this

The most practical lesson for me is that pinning a single-threaded benchmark to a core on an everyday machine is not enough. Affinity worked: the process ran on the requested core. Even so, the elapsed time nearly doubled depending on whether that core was busy. Choosing a low-numbered core means voluntarily joining the busiest location.

I used many short runs and took the minimum because the average was not useful on this machine. Only 22 of 360 runs were undisturbed, so the average was closer to measuring how much Defender was running than to measuring the core.

When benchmark results line up neatly by core number, the thing being measured should be questioned first. I thought I was measuring core performance, but I was measuring Windows thread placement. Next I want to find a period with no load and repeat cpu0 through cpu3. Only then can I discuss whether all 12 are equivalent.

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