Skip to content

gh-151289: Specialize non-compact int add/subtract via BINARY_OP_EXTEND - #159688

Open
KRRT7 wants to merge 1 commit into
python:mainfrom
KRRT7:wide-int-extend
Open

KRRT7 wants to merge 1 commit into
python:mainfrom
KRRT7:wide-int-extend

Conversation

@KRRT7

@KRRT7 KRRT7 commented Oct 18, 2026 •

Copy link
Copy Markdown

int + int and int - int are specialized only when both operands are compact. When either operand is a multi-digit int, BINARY_OP stays unspecialized and goes through the generic binary_op1 dispatch every time.

This adds BINARY_OP_EXTEND descriptors for +, -, += and -= on exact int operands. The specializer still tries the compact path first, so the descriptors only apply to sites where at least one operand was non-compact when the site was specialized. The action calls long_add/long_sub directly through new internal _PyLong_Add/_PyLong_Subtract wrappers.

Design notes:

  • BINARY_OP_ADD_INT, BINARY_OP_SUBTRACT_INT, their guards and the optimizer are unchanged.
  • The descriptor guard is a pure type check (guard == NULL, lhs_type/rhs_type = int), so the optimizer can already drop it when the operand types are known, e.g. in (a - b) - c.
  • The result is typed as int but not as compact, so later compact-int ops keep their guards. This avoids the wrong-result bug found in gh-151289: optimize shrinking wide int subtraction #151290, where a non-compact result was modelled as compact. That case is now a regression test in test_opt.
  • result_unique = 1: long_add/long_sub return either a new object or an immortal small int, which is what the debug assertion in _BINARY_OP_EXTEND checks.
  • Bool and int subclasses fail the exact-type check and are not affected.
  • It does not depend on PyLong_SHIFT.

This follows the same direction as gh-155486, which allowed non-compact ints in TO_BOOL_INT.

Supersedes #151290.

Performance

Wall-clock microbenchmarks on a cloud VM were too noisy for a change this size. PGO layout alone moved untouched compact ops by ±5–8%, and repeated runs of the same binary drifted by up to ~9%. So these are user-space instructions per op (perf stat -e instructions:u, difference between 3M and 1M iterations of a OP b in a loop, empty loop subtracted, 5 repeats each), on 30-bit builds of current main. "sub wide" is 10_000_000_000 - 1, "add wide" is 10_000_000_000 + 1.

instructions/op sub compact add compact sub wide add wide
main (LTO) 94 94 317 317
this PR (LTO) 94 93 278 279
main (PGO+LTO) 96 96 272 263
this PR (PGO+LTO) 97 98 242 233
main (PGO+LTO, JIT) 112 113 300 289
this PR (PGO+LTO, JIT) 112 113 270 258

That's 30–39 fewer instructions per wide add/sub (~10–12%) in every configuration, while compact ops are unchanged. Measured on 7dbc797 and this PR rebased on it.

pyperformance (LTO-only builds of the earlier base c22e9c9, one run each, pinned to an isolated core) shows no measurable change. The geometric mean is 1.03x faster, but benchmarks that do no wide-int arithmetic moved ±10–20% in both directions, so that's noise. This is expected: very little of the suite does non-compact int add/sub. The point of the change is that such code no longer stays on the unspecialized BINARY_OP path, without affecting the compact fast paths.

@KRRT7

KRRT7 commented Oct 18, 2026

Copy link
Copy Markdown
Author

@eendebakpt this is the rework you suggested on #151290: the compact specializations are left exactly as on main, and the multi-digit case goes through BINARY_OP_EXTEND. Your reproducer is included as a regression test in test_opt. Would you be able to take a look?

@KRRT7

KRRT7 commented Oct 18, 2026

Copy link
Copy Markdown
Author

One trade-off worth flagging. Because the BINARY_OP_EXTEND guard here is a pure type check, compact ints pass it too. A site that happens to specialize while it sees a wide value therefore stays on BINARY_OP_EXTEND for compact values as well, and never goes back to BINARY_OP_ADD_INT/BINARY_OP_SUBTRACT_INT. On main, such a site fails to specialize, and on a later attempt it picks up the compact form.

I also built the alternative: a guard function that only accepts exact ints that are not both compact, so compact pairs deopt and respecialize. Instructions per op (perf stat -e instructions:u, same method as in the description, 30-bit builds of 7dbc797):

Extra cost per compact op at a site that was specialized on a wide value:

main this PR (type-only guard) guard function
LTO 0 +34 +1
PGO+LTO +1 +19 +1
PGO+LTO, JIT +63 +36 +87

Instructions saved per wide add/sub (10_000_000_000 ± 1), compared with main:

this PR guard function
LTO 38–39 21–22
PGO+LTO 30–31 11–13
PGO+LTO, JIT 31–33 11–15

The guard function fixes the pinning without the JIT, but it cuts the gain on wide ints by half or more: calling the guard through the pointer and checking compactness costs more. Under the JIT it is worse than both main and this PR in the pinned case, most likely because each compact pair fails the guard inside the trace and exits it (I haven't confirmed that with the JIT's stats). It also stops the optimizer from dropping the guard when the operand types are known.

So I've kept the type-only guard. The pinning only affects sites that specialize while seeing a wide value and later see mostly compact ones, and the cost there is bounded. If you'd rather never be slower than main in the default build, the guard-function version is ready and I can switch to it.

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

Labels

awaiting review type-feature A feature request or enhancement

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant