Skip to content

Commit

Permalink
JIT generator support
Browse files Browse the repository at this point in the history
Summary:
Adds support for JIT generators with 3.12. Note coroutines and async-generators are NOT covered by this diff but should come soon.

Unlike in 3.10, this implementation does not modify `genobject.c` or anything else in the upstream runtime, making it suitable for pip-install. We achieve this by making a new type for JIT generators (implemented in `generators_rt.cpp`) rather than modifying the existing type's features directly.

In theory our new type could be implemented however we want, provided it satisfies the generator API. In practice we must keep the same object data layout and content as the original type to allow for deopt. While this does constrain us, it's also handy as most of the implementation (getters, methods, etc.) are just copied directly from the original generator object. Where we can't copy something because it needs to be different we still have the option to deopt in places and defer back to the original implementation on slow-paths like errors, exceptions, etc.

During development I started by converting our 3.10 implementation to also use a custom type. However, I've discarded this and instead this change only adds this to 3.12. While there are advantages to a unified implementation for 3.10 + 3.12, I think the benefits are outweighed by the 3.10 implementation being quite different and considerably more complicated due the need to support shadow frames. The differences means we're often not getting the benefit of verifying what we're developing for 3.12 on 3.10 and it considerably bloats the implementation and makes it harder to follow. Another minor but irritating difference is in 3.12 there is only one object layout for generators, coroutines, and async-generators. This actually makes life easier in 3.12 but we'd have to do something different to work with the old 3.10 model too.

Most of the existing JIT infrastructure for implementing a generator function is reused with some minor differences. The biggest difference is we immediately allocate the generator object and link it into the Python stack at the start of the function, rather than deferring construction to "initial yield". This new implementation is probably more efficient as it means we can begin using register spill space in the generator object immediately rather than initially using using the stack and then copying the content over later. Another smaller difference is 3.12 provides some more fine-grained opcodes for some generator implementation, but these mostly map to existing concepts so are not hard to implement.

Reviewed By: DinoV

Differential Revision: D66567769

fbshipit-source-id: 8b146489c44e7b3b923f67bdc578a36d797f8367
  • Loading branch information
jbower-fb authored and facebook-github-bot committed Dec 18, 2024
1 parent 5712d9b commit 45f400c
Show file tree
Hide file tree
Showing 2 changed files with 23 additions and 16 deletions.
34 changes: 19 additions & 15 deletions Lib/test/test_generators.py
Original file line number Diff line number Diff line change
Expand Up @@ -206,18 +206,21 @@ def __del__(self):
finally:
gc.set_threshold(*thresholds)

def test_ag_frame_f_back(self):
async def f():
yield
ag = f()
self.assertIsNone(ag.ag_frame.f_back)

def test_cr_frame_f_back(self):
async def f():
pass
cr = f()
self.assertIsNone(cr.cr_frame.f_back)
cr.close() # Suppress RuntimeWarning.
# TODO(T209747648) - These should just be enabled as coroutines and
# async-generators work.
#
# def test_ag_frame_f_back(self):
# async def f():
# yield
# ag = f()
# self.assertIsNone(ag.ag_frame.f_back)
#
# def test_cr_frame_f_back(self):
# async def f():
# pass
# cr = f()
# self.assertIsNone(cr.cr_frame.f_back)
# cr.close() # Suppress RuntimeWarning.

def test_gi_frame_f_back(self):
def f():
Expand Down Expand Up @@ -1043,9 +1046,10 @@ def b():
Implement next(self).
>>> iter(i) is i
True
>>> import types
>>> isinstance(i, types.GeneratorType)
True
>>> # TODO(T209747648) - Maybe this should be replaced with something like
>>> # inspect.isgenerator(). However, that also needs some kind of fix.
>>> #import types
>>> # isinstance(i, types.GeneratorType)
And more, added later.
Expand Down
5 changes: 4 additions & 1 deletion Modules/_testcapimodule.c
Original file line number Diff line number Diff line change
Expand Up @@ -2066,7 +2066,10 @@ raise_SIGINT_then_send_None(PyObject *self, PyObject *args)
{
PyGenObject *gen;

if (!PyArg_ParseTuple(args, "O!", &PyGen_Type, &gen))
// TODO(T209747648) - This was altered to not assert the type of the
// generator. This could maybe be upstreamed in some form a the specific
// type of generator is not important for the test.
if (!PyArg_ParseTuple(args, "O", &gen))
return NULL;

/* This is used in a test to check what happens if a signal arrives just
Expand Down

0 comments on commit 45f400c

Please sign in to comment.