FazBrowse GitHub Viewer | Trending |
URL:
| Home
Tools: [Download Repo ZIP]   [Original HTTPS Page]

GH-136410: Faster side exits by markshannon · Pull Request #136411 · python/cpython · GitHub

/ cpython Public

GH-136410: Faster side exits - #136411

Merged
markshannon merged 15 commits into
python:mainfrom
faster-cpython:fast-side-exits
Aug 1, 2025
Merged

GH-136410: Faster side exits#136411
markshannon merged 15 commits into
python:mainfrom
faster-cpython:fast-side-exits

Conversation

markshannon commented Jul 8, 2025
edited by bedevere-app Bot
Loading

Copy link
Copy Markdown
Member

This PR reinstates the _COLD_EXIT uop, but this time expects the exit to be passed, not the executor.
This way we only need one jitted stub, not hundreds.

Using stubs hugely simplifies _EXIT_TRACE as all it needs to do is jump to the exit's executor.

The x86-64 stencil for _EXIT_TRACE shrinks from 384 bytes to 36 bytes, although it does require one extra _CHECK_VALIDITY (19 bytes) to be added to each trace.
Since traces often contain multiple _EXIT_TRACEs this is a substantial space saving.

Comment thread Python/optimizer.c
// from being immediately detected as cold and invalidated.
cold->vm_data.warm = true;
if (_PyJIT_Compile(cold, cold->trace, 1)) {
Py_DECREF(cold);

Fidget-Spinner Jul 8, 2025
edited
Loading

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

It's immortal. Decrefing won't do anything. I think you mean to call the dealloc function on it?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

Good spot. I think I need to move making it immortal to after it is fully created.

Copy link
Copy Markdown
Member Author

Copy link
Copy Markdown
Member

@pablogsal any ideas what this failure means? https://github.com/python/cpython/actions/runs/16140914160/job/45547999926?pr=136411

Is a race condition in the tests that will be fixed by #136347

Copy link
Copy Markdown
Member Author

Performance is in the noise:

  • Linux x86 +0.4%
  • Windows x86 -0.1%
  • Mac ARM +0.2%

However, I wouldn't expect much of a speedup by reducing the size of exits, so this seems reasonable.

Copy link
Copy Markdown
Member

I think the benchmarking public mirror is down. Can you please share the link when it comes up again?

Copy link
Copy Markdown
Member Author

Note to self: The cold exit executor should be freed when freeing the interpreter to avoid leaking memory.

markshannon requested a review from diegorusso as a code owner July 21, 2025 10:10
Comment thread Python/optimizer.c
executor->exits[i].executor = NULL;
executor->exits[i].index = i;
executor->exits[i].temperature = initial_temperature_backoff_counter();
executor->exits[i].executor = cold;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

We put cold_executor here without INCREF because cold_executor is immortal. But in executor_clear we DECREF each executor from exits because there can be other executors as well. But this is imbalance of INCREF/DECREF. Maybe it is worth to add a comment here that we omit INCREF because of immortality of cold executor?

brandtbucher left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

A couple of notes. I like the general idea, not a huge fan of the jit_exit side-channel (but I get why we have it).

Comment thread Python/optimizer.c
Py_FatalError("Cannot allocate core JIT code");
}
#endif
_Py_SetImmortal((PyObject *)cold);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

Why does it need to be immortal? This means that we'll leak one of these per interpreter, along with about a page of JIT code. I think the interpreter can just hold a normal reference that we free at shutdown, right?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

Then why is it immortal? I'm worried it could get shared at some point, which is generally safe to do with immortal objects. An immortal object that gets cleared at interpreter shutdown seems like it could lead to hard-to-debug problems down the road.

Maybe just not make it immortal, and refcount it normally?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

It is immortal: it outlives all mortal objects in the same interpreter. Reference counting it is just extra overhead.

Comment thread Python/optimizer.c Outdated
Comment thread Python/optimizer.c
Comment thread Python/bytecodes.c
}
exit->temperature = initial_temperature_backoff_counter();
}
assert(tstate->jit_exit == exit);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

Maybe set it to NULL now?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

We could do, but I'd rather leave it pointing to the last exit.
Cold exit relies on jit_exit being the last exit, and I can imagine us adding "compile me now" stubs later that would also rely on it.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

Not a big deal, but I'd lean towards clearing it here. A possibly-dangling pointer seems dangerous, and we could assert it's NULL when setting it to make sure we didn't mess up somewhere.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

It is only meaningful when starting an executor, and it will always have been set to a live executor at that point.
I'd like to keep it this way as it will simplify changing it to be passed in a register with TOS caching.

Comment thread Python/optimizer.c Outdated

bedevere-app Bot commented Jul 30, 2025

Copy link
Copy Markdown

When you're done making the requested changes, leave the comment: I have made the requested changes; please review again.

Copy link
Copy Markdown
Member Author

A couple of notes. I like the general idea, not a huge fan of the jit_exit side-channel (but I get why we have it).

Once we have TOS caching we can pass the exit in one of the cache registers. On occasion we will need to spill a value on the stack, but it should be mostly free.

markshannon commented Jul 30, 2025
edited
Loading

Copy link
Copy Markdown
Member Author

With the changes to _START_EXECUTOR to check for invalidation, _START_EXECUTOR has grown from 0 to 62 bytes (x86-64) which is still pretty good given the savings of 350 bytes per _EXIT_TRACE.

brandtbucher left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

A couple suggestions for possible improvements (and in the discussions above), but nothing blocking. This is a good change, thanks!

Comment on lines +333 to +339
[_START_EXECUTOR] = HAS_DEOPT_FLAG | HAS_ESCAPES_FLAG,
[_MAKE_WARM] = 0,
[_FATAL_ERROR] = 0,
[_DEOPT] = 0,
[_ERROR_POP_N] = HAS_ARG_FLAG,
[_TIER2_RESUME_CHECK] = HAS_DEOPT_FLAG,
[_COLD_EXIT] = HAS_ESCAPES_FLAG,

brandtbucher Jul 31, 2025
edited
Loading

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

Can you mark _PyExecutor_ClearExit and _PyExecutor_FromExit as non-escaping?

...also, am I the only one around here who reviews generated code? ;)

brandtbucher Jul 31, 2025
edited
Loading

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

Ah wait, _PyExecutor_ClearExit can escape.

That's annoying... the traces are going to have a _CHECK_VALIDITY op after the _START_EXECUTOR anyways, because the deopt path in _START_EXECUTOR can escape. But it will never actually reach the next instruction. Seems like something that should be addressed, actually.

Maybe just go back to checking in _EXIT_TRACE like we were before? It's going to get checked anyways, so might as well just do it there.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

...also, am I the only one around here who reviews generated code? ;)

Maybe 🙂

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

Checking validity in _START_EXECUTOR is much more efficient than in _EXIT_TRACE as it is only a single predictable branch.

Comment thread Python/optimizer.c
Comment on lines +1546 to +1548
_PyExecutorObject *e = executor->exits[i].executor;
executor->exits[i].executor = cold;
Py_DECREF(e);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

Could use _PyExecutor_ClearExit here to avoid repeating the same logic twice.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

But that couples the two functions, and they do have distinct uses.

Copy link
Copy Markdown
Member Author

That's annoying... the traces are going to have a _CHECK_VALIDITY op after the _START_EXECUTOR anyways, because the deopt path in _START_EXECUTOR can escape.

It can't really escape, because it is on the deopt path. But that's a separate issue: #137276

markshannon merged commit e7b55f5 into python:main Aug 1, 2025
72 of 73 checks passed
markshannon deleted the fast-side-exits branch August 2, 2025 15:51
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode characters
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants


Back | FazBrowse Home | New Git URL