Skip to content

Commit 90c44bc

Browse files
gh-131798: Support generator frames in the JIT optimizer (GH-143340)
1 parent faa3dc7 commit 90c44bc

File tree

5 files changed

+102
-27
lines changed

5 files changed

+102
-27
lines changed

Include/internal/pycore_tstate.h

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,10 +47,15 @@ typedef struct _PyJitTracerPreviousState {
4747
_PyBloomFilter dependencies;
4848
} _PyJitTracerPreviousState;
4949

50+
typedef struct _PyJitTracerTranslatorState {
51+
int jump_backward_seen;
52+
} _PyJitTracerTranslatorState;
53+
5054
typedef struct _PyJitTracerState {
5155
_PyUOpInstruction *code_buffer;
5256
_PyJitTracerInitialState initial_state;
5357
_PyJitTracerPreviousState prev_state;
58+
_PyJitTracerTranslatorState translator_state;
5459
} _PyJitTracerState;
5560

5661
#endif

Lib/test/test_capi/test_opt.py

Lines changed: 41 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1168,22 +1168,6 @@ def testfunc(n):
11681168
self.assertIsNotNone(ex)
11691169
self.assertIn("_FOR_ITER_TIER_TWO", get_opnames(ex))
11701170

1171-
@unittest.skip("Tracing into generators currently isn't supported.")
1172-
def test_for_iter_gen(self):
1173-
def gen(n):
1174-
for i in range(n):
1175-
yield i
1176-
def testfunc(n):
1177-
g = gen(n)
1178-
s = 0
1179-
for i in g:
1180-
s += i
1181-
return s
1182-
res, ex = self._run_with_optimizer(testfunc, TIER2_THRESHOLD)
1183-
self.assertEqual(res, sum(range(TIER2_THRESHOLD)))
1184-
self.assertIsNotNone(ex)
1185-
self.assertIn("_FOR_ITER_GEN_FRAME", get_opnames(ex))
1186-
11871171
def test_modified_local_is_seen_by_optimized_code(self):
11881172
l = sys._getframe().f_locals
11891173
a = 1
@@ -3404,6 +3388,47 @@ def test_is_none(n):
34043388
self.assertIn("_POP_TOP_NOP", uops)
34053389
self.assertLessEqual(count_ops(ex, "_POP_TOP"), 2)
34063390

3391+
def test_for_iter_gen_frame(self):
3392+
def f(n):
3393+
for i in range(n):
3394+
# Should be optimized to POP_TOP_NOP
3395+
yield i + i
3396+
def testfunc(n):
3397+
for _ in f(n):
3398+
pass
3399+
3400+
res, ex = self._run_with_optimizer(testfunc, TIER2_THRESHOLD*2)
3401+
self.assertIsNotNone(ex)
3402+
uops = get_opnames(ex)
3403+
3404+
self.assertIn("_FOR_ITER_GEN_FRAME", uops)
3405+
# _POP_TOP_NOP is a sign the optimizer ran and didn't hit bottom.
3406+
self.assertGreaterEqual(count_ops(ex, "_POP_TOP_NOP"), 1)
3407+
3408+
def test_send_gen_frame(self):
3409+
3410+
def gen(n):
3411+
for i in range(n):
3412+
yield i + i
3413+
def send_gen(n):
3414+
yield from gen(n)
3415+
def testfunc(n):
3416+
for _ in send_gen(n):
3417+
pass
3418+
3419+
for _ in range(_testinternalcapi.SPECIALIZATION_THRESHOLD):
3420+
# Ensure SEND is specialized to SEND_GEN
3421+
send_gen(10)
3422+
3423+
res, ex = self._run_with_optimizer(testfunc, TIER2_THRESHOLD*2)
3424+
self.assertIsNotNone(ex)
3425+
uops = get_opnames(ex)
3426+
3427+
self.assertIn("_FOR_ITER_GEN_FRAME", uops)
3428+
self.assertIn("_SEND_GEN_FRAME", uops)
3429+
# _POP_TOP_NOP is a sign the optimizer ran and didn't hit bottom.
3430+
self.assertGreaterEqual(count_ops(ex, "_POP_TOP_NOP"), 1)
3431+
34073432
def test_143026(self):
34083433
# https://github.com/python/cpython/issues/143026
34093434

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
The JIT optimizer now understands more generator instructions.

Python/optimizer_bytecodes.c

Lines changed: 27 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -939,15 +939,35 @@ dummy_func(void) {
939939
}
940940

941941
op(_FOR_ITER_GEN_FRAME, (unused, unused -- unused, unused, gen_frame)) {
942-
gen_frame = PyJitRef_NULL;
943-
/* We are about to hit the end of the trace */
944-
ctx->done = true;
942+
assert((this_instr + 1)->opcode == _PUSH_FRAME);
943+
PyCodeObject *co = get_code_with_logging((this_instr + 1));
944+
if (co == NULL) {
945+
ctx->done = true;
946+
break;
947+
}
948+
_Py_UOpsAbstractFrame *new_frame = frame_new(ctx, co, 1, NULL, 0);
949+
if (new_frame == NULL) {
950+
ctx->done = true;
951+
break;
952+
}
953+
new_frame->stack[0] = sym_new_const(ctx, Py_None);
954+
gen_frame = PyJitRef_Wrap((JitOptSymbol *)new_frame);
945955
}
946956

947-
op(_SEND_GEN_FRAME, (unused, unused -- unused, gen_frame)) {
948-
gen_frame = PyJitRef_NULL;
949-
// We are about to hit the end of the trace:
950-
ctx->done = true;
957+
op(_SEND_GEN_FRAME, (unused, v -- unused, gen_frame)) {
958+
assert((this_instr + 1)->opcode == _PUSH_FRAME);
959+
PyCodeObject *co = get_code_with_logging((this_instr + 1));
960+
if (co == NULL) {
961+
ctx->done = true;
962+
break;
963+
}
964+
_Py_UOpsAbstractFrame *new_frame = frame_new(ctx, co, 1, NULL, 0);
965+
if (new_frame == NULL) {
966+
ctx->done = true;
967+
break;
968+
}
969+
new_frame->stack[0] = PyJitRef_StripReferenceInfo(v);
970+
gen_frame = PyJitRef_Wrap((JitOptSymbol *)new_frame);
951971
}
952972

953973
op(_CHECK_STACK_SPACE, (unused, unused, unused[oparg] -- unused, unused, unused[oparg])) {

Python/optimizer_cases.c.h

Lines changed: 28 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)