Skip to content

Optimize int add/sub for wide exact ints #151289

Description

@KRRT7

CPython has a fast path for compact integers in binary add/sub, but wide exact ints still go through the generic long arithmetic path even when both operands fit in int64_t.

This issue proposes adding a separate fast path for exact PyLong operands that fit in signed 64-bit integers, while preserving the existing compact-int path.

Suggested implementation:

  • Keep the current compact-int specialization unchanged.
  • Add a separate wide-int path for exact ints that fit in int64_t.
  • Preserve current behavior for overflow, subclasses, and other non-exact-int cases.

Motivation:

  • Improve performance for wide integer add/sub without affecting the common compact-int hot path.
  • Avoid adding new opcodes in the compact-int path.
  • Fit within the current interpreter and specialization structure.

Benchmark evidence:

  • I prototyped this locally with a benchmark covering compact and wide add/sub cases.
  • Wide cases improved substantially, while compact cases remained effectively flat.
  • Representative interpreter-only results with JIT disabled:
    • add_wide: about 25% faster
    • sub_wide: about 35% faster
    • add_compact/sub_compact: effectively unchanged

Benchmark script used locally:

"""Microbenchmark compact vs wide int add/sub with pyperf.

Use this with PYTHON_JIT=0 and -S if you want a stable interpreter-only run:

    PYTHON_JIT=0 ./python.exe -S Tools/scripts/bench_wide_int_pyperf.py
"""

from __future__ import annotations

import pyperf


def bench_add_compact() -> int:
    a = 1
    b = 2
    return a + b


def bench_add_wide() -> int:
    a = 10_000_000_000
    b = 1
    return a + b


def bench_sub_compact() -> int:
    a = 1
    b = 2
    return a - b


def bench_sub_wide() -> int:
    a = 10_000_000_000
    b = 1
    return a - b


def main() -> None:
    runner = pyperf.Runner()
    runner.bench_func("add_compact", bench_add_compact)
    runner.bench_func("add_wide", bench_add_wide)
    runner.bench_func("sub_compact", bench_sub_compact)
    runner.bench_func("sub_wide", bench_sub_wide)


if __name__ == "__main__":
    main()

Linked PRs

Activity

  1. KRRT7 commented on Jun 10, 2026

    @KRRT7
    Author

    came out of #150425 but since the JIT is a no go zone for the near future ¯_(ツ)_/¯

  2. added
    type-featureA feature request or enhancement
    performancePerformance or resource usage
    interpreter-core(Objects, Python, Grammar, and Parser dirs)
    on Jun 11, 2026
  3. skirpichev commented on Jun 11, 2026

    @skirpichev
    Member

    I would prefer if you share results of the pyperformance on this. Micro-benchmarks in #150425 looks impressive, but the pyperformance seems to be not affected (or even shows worse results).

    So far, I'm not sure that this optimization worth code complications.

    Perhaps, it will be better if we first change PyLong internals to use tagged pointers, it was recently come again in this d.p.o thread:
    https://discuss.python.org/t/pylongobject-just-use-gmp/106086/29

    With this, we will have essentially only two code paths for machine-sized integers and for big integers. Just like now.

  4. KRRT7 commented on Jun 11, 2026

    @KRRT7
    Author

    ah, my bad @skirpichev, I had the message drafted but it wasn't sent:

    (.bench) root@codespaces-1df16c:/workspaces/cpython# .bench/bin/python -m pyperf compare_to baseline.json candidate.json
    add_compact: Mean +- std dev: [baseline] 251 us +- 20 us -> [candidate] 234 us +- 18 us: 1.07x faster
    add_wide: Mean +- std dev: [baseline] 505 us +- 42 us -> [candidate] 372 us +- 27 us: 1.36x faster
    sub_compact: Mean +- std dev: [baseline] 261 us +- 33 us -> [candidate] 238 us +- 19 us: 1.09x faster
    sub_wide: Mean +- std dev: [baseline] 511 us +- 35 us -> [candidate] 389 us +- 48 us: 1.31x faster
    chain_wide: Mean +- std dev: [baseline] 1.40 ms +- 0.11 ms -> [candidate] 1.06 ms +- 0.09 ms: 1.32x faster
    
    Benchmark hidden because not significant (1): chain_compact
    
    Geometric mean: 1.18x faster
    
    
  5. KRRT7 commented on Jun 11, 2026

    @KRRT7
    Author

    Perhaps, it will be better if we first change PyLong internals to use tagged pointers, it was recently come again in this d.p.o thread:

    actually I was planning to follow up in the future with that, I believe we can leverage both implementations.

  6. KRRT7 commented on Jun 11, 2026

    @KRRT7
    Author

    you should consider the benchmarks in #150425 separate and probably irrelevant for the next few months since that touches the tier 2 JIT

  7. skirpichev commented on Jun 11, 2026

    @skirpichev
    Member

    I had the message drafted but it wasn't sent

    Thanks, but as I said - micro-benchmarks are less interesting.

    actually I was planning to follow up in the future with that, I believe we can leverage both implementations.

    I'm not sure. If we will use separate representation for machine-sized integers (take look on FLINT's fmpz), I don't think we will have need in a third code path ("wide ints").

    CC @tim-one

    you should consider the benchmarks in #150425 separate and probably irrelevant for the next few months since that touches the tier 2 JIT

    PYTHON_JIT=0 should be relevant, no?

  8. added 2 commits that reference this issue on Jul 10, 2026
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

    interpreter-core(Objects, Python, Grammar, and Parser dirs)performancePerformance or resource usagetype-featureA feature request or enhancement

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions