Skip to content

Windows ARM64 tail-call interpreter: compiler and commit performance effects #159521

Open

Description

@diegorusso

While testing and measuring the tail-call interpreter on Windows ARM64, with the goal of enabling it by default on WoA, I noticed a performance change across commit ad1513a, "Reduce the work done to spill and reload the stack around calls".
The change avoids redundant stores and reloads of the interpreter's stack pointer around calls, keeping that pointer live across calls and increasing register pressure. Changing the compiler from MSVC 14.51 to 14.52 also affects performance.

I wanted to separate these effects before quoting a tail-call speedup.
The detailed results below are native ARM64 on a Windows Dev Kit 2023, pinned to performance-class CPU.

In terms of commits, both revisions are 3.16.0a0:

1. Effect of the commit and compiler version on the switch interpreter

Put all four switch based builds on one common scale: before-commit MSVC 14.51 = 100. Higher means faster.

Source revision MSVC 14.51 speed index MSVC 14.52 speed index
Before the commit 100.00 100.43
After the commit 92.49 96.80

Before the commit, the compilers produce almost equally fast builds. After the commit, both switch builds slow down: by 7.51% with MSVC 14.51 and 3.62% with MSVC 14.52, each relative to its own before-commit build.
MSVC 14.52 partly mitigates the switch interpreter regression introduced by the commit. Its larger lead after the commit reflects losing less performance across that change. We have measured this interaction; we have not yet established which difference in generated machine code explains it.

Emulated x64. Testing x64 binaries under Windows emulation on the same ARM64 device also shows a switch slowdown across the commit with both compilers. In those runs, 14.52 does not provide the mitigation observed on native ARM64.

The overall result varies across individual benchmarks: asyncio_websockets shows a measured speed loss of about 56% with 14.52 on both revisions.

Most affected benchmarks when changing the compiler: 14.52 versus 14.51

Each row below compares the switch build made with MSVC 14.52 against the switch build made with MSVC 14.51 at the same source revision. Positive means faster; negative means slower. † marks high variability (coefficient of variation above 10% in either compared build).

Source revision Largest measured gains Largest measured losses
Before the commit regex_effbot (+6.87%); base85_large (+5.88%); nqueens (+5.21%) asyncio_websockets (-55.85%); tornado_http† (-33.75%); ascii85_large (-4.71%)
After the commit generators (+30.37%); unpack_sequence (+28.93%); nbody (+16.72%) asyncio_websockets (-55.68%); tornado_http† (-22.28%); unpickle_list (-4.45%)
Most affected benchmarks across the commit: after versus before

Each row below compares the after-commit switch build against its before-commit switch build, keeping the compiler unchanged. Positive means faster; negative means slower. † marks high variability (coefficient of variation above 10% in either compared build).

Compiler Largest measured gains Largest measured losses
MSVC 14.51 python_startup_no_site† (+1.65%); gc_traversal (+0.93%); regex_dna (+0.45%) generators (-29.42%); tornado_http† (-25.86%); nbody (-25.79%)
MSVC 14.52 unpack_sequence (+2.76%); create_gc_cycles (+0.88%); gc_traversal (+0.59%) spectral_norm (-17.16%); nbody (-15.87%); tornado_http† (-13.03%)

2. Tail-call versus switch: MSVC 14.52

Change only the interpreter. Both builds use MSVC 14.52. For each revision, the baseline is the switch build at that same revision.

Source revision Tail-call build, compared with switch build using the same source and compiler
Before the commit 4.43% faster
After the commit 9.36% faster

Tail-call is faster than switch on both revisions: by 4.43% before the commit and 9.36% after it. These are two comparisons against the switch build at the corresponding revision. The previous table shows that the commit slows switch, which contributes to the larger after-commit tail-call advantage.
A claim of "9.36% faster" should therefore specify the MSVC 14.52 after-commit switch baseline. This is context for reporting relative speedups whilst keeping the goal to enabling tail-call by default on WoA.

Emulated x64. With MSVC 14.52 under x64 emulation, tail-call's advantage over switch at the same revision grows from 11.47% before the commit to 17.43% afterwards, mainly because switch slows down while tail-call speed changes little. These are relative comparisons within emulation; the percentages need confirmation on native x64 hardware.

Most affected benchmarks: three largest gains and losses per comparison

These use the same baseline as the table above. Positive means faster; negative means slower. † marks high variability (coefficient of variation above 10% in either compared build).

Source revision Largest measured gains Largest measured losses
Before the commit tornado_http† (+55.63%); hexiom (+19.41%); deltablue (+16.50%) dulwich_log (-0.17%); base85_large (-0.08%); ascii85_large (-0.07%)
After the commit tornado_http† (+112.05%); spectral_norm (+30.13%); nbody (+28.81%) pathlib† (-7.23%); asyncio_tcp (-2.25%); gc_traversal (-0.75%)

The tail-call interpreter should be enabled by default on Windows on Arm. Speedup claims should be precise and explicitly state the baseline, including the CPython revision, compiler version, and interpreter configuration.

I'd also welcome someone repeating these experiments on native x86-64 (x64) hardware running Windows, as I don't currently have access to such a machine. :)
I've attached the scripts I have used to run these experiments compiler-matrix.zip
It's easy to run:

py -3.14 .\run_compiler_matrix.py `
    --run-dir C:\path\to\run-results `
    --architectures "x64" `
    --benchmarks all --affinity X

Activity

  1. diegorusso commented on Oct 16, 2026

    @diegorusso
    ContributorAuthor

    Some feedback I received: we should build and test PGO builds. The above results are without it. I've put the scripts in a gist for now that should address the PGO and the plots (everyone likes plots).

    Here's the link: https://gist.github.com/diegorusso/3a0cfca565ad919ff094c88778cc17bd

    I'm re-running the benchmarks again and update the above test with the new results.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions