Skip to content

Commit 5aee627

Browse files
Cover generator frames in the JIT optimizer
1 parent e5ad7b7 commit 5aee627

File tree

5 files changed

+120
-34
lines changed

5 files changed

+120
-34
lines changed

Include/internal/pycore_tstate.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ typedef struct _PyJitTracerPreviousState {
4545
PyCodeObject *instr_code; // Strong
4646
struct _PyInterpreterFrame *instr_frame;
4747
_PyBloomFilter dependencies;
48+
int jump_backward_seen;
4849
} _PyJitTracerPreviousState;
4950

5051
typedef struct _PyJitTracerState {

Lib/test/test_capi/test_opt.py

Lines changed: 50 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,9 @@ def iter_ops(ex):
6767
def get_ops(ex):
6868
return list(iter_ops(ex))
6969

70+
def count_ops(ex, name):
71+
return len([opname for opname in iter_opnames(ex) if opname == name])
72+
7073

7174
@requires_specialization
7275
@unittest.skipIf(Py_GIL_DISABLED, "optimizer not yet supported in free-threaded builds")
@@ -1165,22 +1168,6 @@ def testfunc(n):
11651168
self.assertIsNotNone(ex)
11661169
self.assertIn("_FOR_ITER_TIER_TWO", get_opnames(ex))
11671170

1168-
@unittest.skip("Tracing into generators currently isn't supported.")
1169-
def test_for_iter_gen(self):
1170-
def gen(n):
1171-
for i in range(n):
1172-
yield i
1173-
def testfunc(n):
1174-
g = gen(n)
1175-
s = 0
1176-
for i in g:
1177-
s += i
1178-
return s
1179-
res, ex = self._run_with_optimizer(testfunc, TIER2_THRESHOLD)
1180-
self.assertEqual(res, sum(range(TIER2_THRESHOLD)))
1181-
self.assertIsNotNone(ex)
1182-
self.assertIn("_FOR_ITER_GEN_FRAME", get_opnames(ex))
1183-
11841171
def test_modified_local_is_seen_by_optimized_code(self):
11851172
l = sys._getframe().f_locals
11861173
a = 1
@@ -3302,6 +3289,53 @@ def test_is_none(n):
33023289
self.assertIn("_POP_TOP_NOP", uops)
33033290
self.assertNotIn("_POP_TOP", uops)
33043291

3292+
def test_for_iter_gen_frame(self):
3293+
def f(n):
3294+
for i in range(n):
3295+
# Should be optimized to POP_TOP_NOP
3296+
yield i + i
3297+
def testfunc(n):
3298+
for _ in f(n):
3299+
pass
3300+
3301+
res, ex = self._run_with_optimizer(testfunc, TIER2_THRESHOLD*2)
3302+
self.assertIsNotNone(ex)
3303+
uops = get_opnames(ex)
3304+
3305+
self.assertIn("_FOR_ITER_GEN_FRAME", uops)
3306+
self.assertIn("_YIELD_VALUE", uops)
3307+
# It's essential for performance that the trace loops around.
3308+
self.assertIn("_JUMP_TO_TOP", uops)
3309+
# _POP_TOP_NOP is a sign the optimizer ran and didn't hit bottom.
3310+
self.assertGreaterEqual(count_ops(ex, "_POP_TOP_NOP"), 3)
3311+
3312+
def test_send_gen_frame(self):
3313+
3314+
def gen(n):
3315+
for i in range(n):
3316+
yield i + i
3317+
def send_gen(n):
3318+
yield from gen(n)
3319+
def testfunc(n):
3320+
for _ in send_gen(n):
3321+
pass
3322+
3323+
for _ in range(_testinternalcapi.SPECIALIZATION_THRESHOLD):
3324+
# Ensure SEND is specialized to SEND_GEN
3325+
send_gen(10)
3326+
3327+
res, ex = self._run_with_optimizer(testfunc, TIER2_THRESHOLD*2)
3328+
self.assertIsNotNone(ex)
3329+
uops = get_opnames(ex)
3330+
3331+
self.assertIn("_FOR_ITER_GEN_FRAME", uops)
3332+
self.assertIn("_YIELD_VALUE", uops)
3333+
self.assertIn("_SEND_GEN_FRAME", uops)
3334+
# It's essential for performance that the trace loops around.
3335+
self.assertIn("_JUMP_TO_TOP", uops)
3336+
# _POP_TOP_NOP is a sign the optimizer ran and didn't hit bottom.
3337+
self.assertGreaterEqual(count_ops(ex, "_POP_TOP_NOP"), 2)
3338+
33053339
def test_143026(self):
33063340
# https://github.com/python/cpython/issues/143026
33073341

Python/optimizer.c

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -823,16 +823,22 @@ _PyJit_translate_single_bytecode_to_trace(
823823
_tstate->jit_tracer_state.initial_state.exit == NULL &&
824824
// These are coroutines, and we want to unroll those usually.
825825
opcode != JUMP_BACKWARD_NO_INTERRUPT) {
826-
// We encountered a JUMP_BACKWARD but not to the top of our own loop.
826+
// We encountered a second JUMP_BACKWARD but not to the top of our own loop.
827827
// We don't want to continue tracing as we might get stuck in the
828828
// inner loop. Instead, end the trace where the executor of the
829829
// inner loop might start and let the traces rejoin.
830-
OPT_STAT_INC(inner_loop);
831-
ADD_TO_TRACE(_EXIT_TRACE, 0, 0, target);
832-
trace[trace_length-1].operand1 = true; // is_control_flow
833-
DPRINTF(2, "JUMP_BACKWARD not to top ends trace %p %p %p\n", next_instr,
834-
_tstate->jit_tracer_state.initial_state.close_loop_instr, _tstate->jit_tracer_state.initial_state.start_instr);
835-
goto done;
830+
if (_tstate->jit_tracer_state.prev_state.jump_backward_seen >= 1) {
831+
OPT_STAT_INC(inner_loop);
832+
ADD_TO_TRACE(_EXIT_TRACE, 0, 0, target);
833+
trace[trace_length-1].operand1 = true; // is_control_flow
834+
DPRINTF(2, "JUMP_BACKWARD not to top ends trace %p %p %p\n", next_instr,
835+
_tstate->jit_tracer_state.initial_state.close_loop_instr, _tstate->jit_tracer_state.initial_state.start_instr);
836+
goto done;
837+
}
838+
else {
839+
assert(_tstate->jit_tracer_state.prev_state.jump_backward_seen == 0);
840+
_tstate->jit_tracer_state.prev_state.jump_backward_seen++;
841+
}
836842
}
837843
break;
838844
}
@@ -1064,6 +1070,7 @@ _PyJit_TryInitializeTracing(
10641070
_tstate->jit_tracer_state.initial_state.exit = exit;
10651071
_tstate->jit_tracer_state.initial_state.stack_depth = curr_stackdepth;
10661072
_tstate->jit_tracer_state.initial_state.chain_depth = chain_depth;
1073+
_tstate->jit_tracer_state.prev_state.jump_backward_seen = 0;
10671074
_tstate->jit_tracer_state.prev_state.instr_frame = frame;
10681075
_tstate->jit_tracer_state.prev_state.dependencies_still_valid = true;
10691076
_tstate->jit_tracer_state.prev_state.instr_code = (PyCodeObject *)Py_NewRef(_PyFrame_GetCode(frame));

Python/optimizer_bytecodes.c

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

929929
op(_FOR_ITER_GEN_FRAME, (unused, unused -- unused, unused, gen_frame)) {
930-
gen_frame = PyJitRef_NULL;
931-
/* We are about to hit the end of the trace */
932-
ctx->done = true;
930+
assert((this_instr + 1)->opcode == _PUSH_FRAME);
931+
PyCodeObject *co = get_code_with_logging((this_instr + 1));
932+
if (co == NULL) {
933+
ctx->done = true;
934+
break;
935+
}
936+
_Py_UOpsAbstractFrame *new_frame = frame_new(ctx, co, 1, NULL, 0);
937+
if (new_frame == NULL) {
938+
ctx->done = true;
939+
break;
940+
}
941+
new_frame->stack[0] = sym_new_const(ctx, Py_None);
942+
gen_frame = PyJitRef_Wrap((JitOptSymbol *)new_frame);
933943
}
934944

935-
op(_SEND_GEN_FRAME, (unused, unused -- unused, gen_frame)) {
936-
gen_frame = PyJitRef_NULL;
937-
// We are about to hit the end of the trace:
938-
ctx->done = true;
945+
op(_SEND_GEN_FRAME, (unused, v -- unused, gen_frame)) {
946+
assert((this_instr + 1)->opcode == _PUSH_FRAME);
947+
PyCodeObject *co = get_code_with_logging((this_instr + 1));
948+
if (co == NULL) {
949+
ctx->done = true;
950+
break;
951+
}
952+
_Py_UOpsAbstractFrame *new_frame = frame_new(ctx, co, 1, NULL, 0);
953+
if (new_frame == NULL) {
954+
ctx->done = true;
955+
break;
956+
}
957+
new_frame->stack[0] = PyJitRef_StripReferenceInfo(v);
958+
gen_frame = PyJitRef_Wrap((JitOptSymbol *)new_frame);
939959
}
940960

941961
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)