Skip to content

Commit dd1884d

Browse files
authored
gh-106529: Split FOR_ITER_RANGE into uops (#106638)
For an example of what this does for Tier 1 and Tier 2, see #106529 (comment)
1 parent 7f55f58 commit dd1884d

File tree

6 files changed

+146
-28
lines changed

6 files changed

+146
-28
lines changed

Include/internal/pycore_opcode_metadata.h

+6
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Lib/test/test_capi/test_misc.py

+22-3
Original file line numberDiff line numberDiff line change
@@ -2443,7 +2443,6 @@ def testfunc(x):
24432443
i += 1
24442444

24452445
opt = _testinternalcapi.get_uop_optimizer()
2446-
24472446
with temporary_optimizer(opt):
24482447
testfunc(1000)
24492448

@@ -2580,13 +2579,33 @@ def testfunc(n):
25802579

25812580
ex = get_first_executor(testfunc)
25822581
self.assertIsNotNone(ex)
2583-
# for i, (opname, oparg) in enumerate(ex):
2584-
# print(f"{i:4d}: {opname:<20s} {oparg:4d}")
25852582
uops = {opname for opname, _ in ex}
25862583
# Since there is no JUMP_FORWARD instruction,
25872584
# look for indirect evidence: the += operator
25882585
self.assertIn("_BINARY_OP_ADD_INT", uops)
25892586

2587+
def test_for_iter_range(self):
2588+
def testfunc(n):
2589+
total = 0
2590+
for i in range(n):
2591+
total += i
2592+
return total
2593+
# import dis; dis.dis(testfunc)
2594+
2595+
opt = _testinternalcapi.get_uop_optimizer()
2596+
with temporary_optimizer(opt):
2597+
total = testfunc(10)
2598+
self.assertEqual(total, 45)
2599+
2600+
ex = get_first_executor(testfunc)
2601+
self.assertIsNotNone(ex)
2602+
# for i, (opname, oparg) in enumerate(ex):
2603+
# print(f"{i:4d}: {opname:<20s} {oparg:3d}")
2604+
uops = {opname for opname, _ in ex}
2605+
self.assertIn("_ITER_EXHAUSTED_RANGE", uops)
2606+
# Verification that the jump goes past END_FOR
2607+
# is done by manual inspection of the output
2608+
25902609

25912610
if __name__ == "__main__":
25922611
unittest.main()

Python/bytecodes.c

+23-4
Original file line numberDiff line numberDiff line change
@@ -2451,9 +2451,14 @@ dummy_func(
24512451
// Common case: no jump, leave it to the code generator
24522452
}
24532453

2454-
inst(FOR_ITER_RANGE, (unused/1, iter -- iter, next)) {
2454+
op(_ITER_CHECK_RANGE, (iter -- iter)) {
24552455
_PyRangeIterObject *r = (_PyRangeIterObject *)iter;
24562456
DEOPT_IF(Py_TYPE(r) != &PyRangeIter_Type, FOR_ITER);
2457+
}
2458+
2459+
op(_ITER_JUMP_RANGE, (iter -- iter)) {
2460+
_PyRangeIterObject *r = (_PyRangeIterObject *)iter;
2461+
assert(Py_TYPE(r) == &PyRangeIter_Type);
24572462
STAT_INC(FOR_ITER, hit);
24582463
if (r->len <= 0) {
24592464
STACK_SHRINK(1);
@@ -2463,15 +2468,29 @@ dummy_func(
24632468
JUMPBY(oparg + 1);
24642469
DISPATCH();
24652470
}
2471+
}
2472+
2473+
// Only used by Tier 2
2474+
op(_ITER_EXHAUSTED_RANGE, (iter -- iter, exhausted)) {
2475+
_PyRangeIterObject *r = (_PyRangeIterObject *)iter;
2476+
assert(Py_TYPE(r) == &PyRangeIter_Type);
2477+
exhausted = r->len <= 0 ? Py_True : Py_False;
2478+
}
2479+
2480+
op(_ITER_NEXT_RANGE, (iter -- iter, next)) {
2481+
_PyRangeIterObject *r = (_PyRangeIterObject *)iter;
2482+
assert(Py_TYPE(r) == &PyRangeIter_Type);
2483+
assert(r->len > 0);
24662484
long value = r->start;
24672485
r->start = value + r->step;
24682486
r->len--;
24692487
next = PyLong_FromLong(value);
2470-
if (next == NULL) {
2471-
goto error;
2472-
}
2488+
ERROR_IF(next == NULL, error);
24732489
}
24742490

2491+
macro(FOR_ITER_RANGE) =
2492+
unused/1 + _ITER_CHECK_RANGE + _ITER_JUMP_RANGE + _ITER_NEXT_RANGE;
2493+
24752494
inst(FOR_ITER_GEN, (unused/1, iter -- iter, unused)) {
24762495
DEOPT_IF(tstate->interp->eval_frame, FOR_ITER);
24772496
PyGenObject *gen = (PyGenObject *)iter;

Python/executor_cases.c.h

+34
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Python/generated_cases.c.h

+38-20
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Python/optimizer.c

+23-1
Original file line numberDiff line numberDiff line change
@@ -479,6 +479,28 @@ translate_bytecode_to_trace(
479479
break;
480480
}
481481

482+
case FOR_ITER_RANGE:
483+
{
484+
// Assume jump unlikely (can a for-loop exit be likely?)
485+
// Reserve 9 entries (4 here, 3 stub, plus SAVE_IP + EXIT_TRACE)
486+
if (trace_length + 9 > max_length) {
487+
DPRINTF(1, "Ran out of space for FOR_ITER_RANGE\n");
488+
goto done;
489+
}
490+
_Py_CODEUNIT *target_instr = // +1 at the end skips over END_FOR
491+
instr + 1 + _PyOpcode_Caches[_PyOpcode_Deopt[opcode]] + oparg + 1;
492+
max_length -= 3; // Really the start of the stubs
493+
ADD_TO_TRACE(_ITER_CHECK_RANGE, 0);
494+
ADD_TO_TRACE(_ITER_EXHAUSTED_RANGE, 0);
495+
ADD_TO_TRACE(_POP_JUMP_IF_TRUE, max_length);
496+
ADD_TO_TRACE(_ITER_NEXT_RANGE, 0);
497+
498+
ADD_TO_STUB(max_length + 0, POP_TOP, 0);
499+
ADD_TO_STUB(max_length + 1, SAVE_IP, INSTR_IP(target_instr, code));
500+
ADD_TO_STUB(max_length + 2, EXIT_TRACE, 0);
501+
break;
502+
}
503+
482504
default:
483505
{
484506
const struct opcode_macro_expansion *expansion = &_PyOpcode_macro_expansion[opcode];
@@ -574,8 +596,8 @@ translate_bytecode_to_trace(
574596
}
575597
}
576598
}
577-
trace_length += buffer_size - max_length;
578599
}
600+
trace_length += buffer_size - max_length;
579601
return trace_length;
580602
}
581603
else {

0 commit comments

Comments
 (0)