Skip to content

Commit 160f2fe

Browse files
authored
GH-87849: Simplify stack effect of SEND and specialize it for generators and coroutines. (GH-101788)
1 parent a1f08f5 commit 160f2fe

15 files changed

+185
-99
lines changed

Include/internal/pycore_code.h

+7
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,12 @@ typedef struct {
9292

9393
#define INLINE_CACHE_ENTRIES_FOR_ITER CACHE_ENTRIES(_PyForIterCache)
9494

95+
typedef struct {
96+
uint16_t counter;
97+
} _PySendCache;
98+
99+
#define INLINE_CACHE_ENTRIES_SEND CACHE_ENTRIES(_PySendCache)
100+
95101
// Borrowed references to common callables:
96102
struct callable_cache {
97103
PyObject *isinstance;
@@ -233,6 +239,7 @@ extern void _Py_Specialize_CompareAndBranch(PyObject *lhs, PyObject *rhs,
233239
extern void _Py_Specialize_UnpackSequence(PyObject *seq, _Py_CODEUNIT *instr,
234240
int oparg);
235241
extern void _Py_Specialize_ForIter(PyObject *iter, _Py_CODEUNIT *instr, int oparg);
242+
extern void _Py_Specialize_Send(PyObject *receiver, _Py_CODEUNIT *instr);
236243

237244
/* Finalizer function for static codeobjects used in deepfreeze.py */
238245
extern void _PyStaticCode_Fini(PyCodeObject *co);

Include/internal/pycore_opcode.h

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

Include/opcode.h

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

Lib/dis.py

+5-4
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
BINARY_OP = opmap['BINARY_OP']
4040
JUMP_BACKWARD = opmap['JUMP_BACKWARD']
4141
FOR_ITER = opmap['FOR_ITER']
42+
SEND = opmap['SEND']
4243
LOAD_ATTR = opmap['LOAD_ATTR']
4344

4445
CACHE = opmap["CACHE"]
@@ -453,6 +454,7 @@ def _get_instructions_bytes(code, varname_from_oparg=None,
453454
argrepr = ''
454455
positions = Positions(*next(co_positions, ()))
455456
deop = _deoptop(op)
457+
caches = _inline_cache_entries[deop]
456458
if arg is not None:
457459
# Set argval to the dereferenced value of the argument when
458460
# available, and argrepr to the string representation of argval.
@@ -478,8 +480,7 @@ def _get_instructions_bytes(code, varname_from_oparg=None,
478480
elif deop in hasjrel:
479481
signed_arg = -arg if _is_backward_jump(deop) else arg
480482
argval = offset + 2 + signed_arg*2
481-
if deop == FOR_ITER:
482-
argval += 2
483+
argval += 2 * caches
483484
argrepr = "to " + repr(argval)
484485
elif deop in haslocal or deop in hasfree:
485486
argval, argrepr = _get_name_info(arg, varname_from_oparg)
@@ -633,12 +634,12 @@ def findlabels(code):
633634
for offset, op, arg in _unpack_opargs(code):
634635
if arg is not None:
635636
deop = _deoptop(op)
637+
caches = _inline_cache_entries[deop]
636638
if deop in hasjrel:
637639
if _is_backward_jump(deop):
638640
arg = -arg
639641
label = offset + 2 + arg*2
640-
if deop == FOR_ITER:
641-
label += 2
642+
label += 2 * caches
642643
elif deop in hasjabs:
643644
label = arg*2
644645
else:

Lib/importlib/_bootstrap_external.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -432,6 +432,7 @@ def _write_atomic(path, data, mode=0o666):
432432
# Python 3.12a5 3516 (Add COMPARE_AND_BRANCH instruction)
433433
# Python 3.12a5 3517 (Change YIELD_VALUE oparg to exception block depth)
434434
# Python 3.12a5 3518 (Add RETURN_CONST instruction)
435+
# Python 3.12a5 3519 (Modify SEND instruction)
435436

436437
# Python 3.13 will start with 3550
437438

@@ -444,7 +445,7 @@ def _write_atomic(path, data, mode=0o666):
444445
# Whenever MAGIC_NUMBER is changed, the ranges in the magic_values array
445446
# in PC/launcher.c must also be updated.
446447

447-
MAGIC_NUMBER = (3518).to_bytes(2, 'little') + b'\r\n'
448+
MAGIC_NUMBER = (3519).to_bytes(2, 'little') + b'\r\n'
448449

449450
_RAW_MAGIC_NUMBER = int.from_bytes(MAGIC_NUMBER, 'little') # For import.c
450451

Lib/opcode.py

+7-1
Original file line numberDiff line numberDiff line change
@@ -167,7 +167,7 @@ def pseudo_op(name, op, real_ops):
167167
def_op('RETURN_CONST', 121)
168168
hasconst.append(121)
169169
def_op('BINARY_OP', 122)
170-
jrel_op('SEND', 123) # Number of bytes to skip
170+
jrel_op('SEND', 123) # Number of words to skip
171171
def_op('LOAD_FAST', 124) # Local variable number, no null check
172172
haslocal.append(124)
173173
def_op('STORE_FAST', 125) # Local variable number
@@ -370,6 +370,9 @@ def pseudo_op(name, op, real_ops):
370370
"UNPACK_SEQUENCE_TUPLE",
371371
"UNPACK_SEQUENCE_TWO_TUPLE",
372372
],
373+
"SEND": [
374+
"SEND_GEN",
375+
],
373376
}
374377
_specialized_instructions = [
375378
opcode for family in _specializations.values() for opcode in family
@@ -429,6 +432,9 @@ def pseudo_op(name, op, real_ops):
429432
"STORE_SUBSCR": {
430433
"counter": 1,
431434
},
435+
"SEND": {
436+
"counter": 1,
437+
},
432438
}
433439

434440
_inline_cache_entries = [

Lib/test/test_dis.py

+15-10
Original file line numberDiff line numberDiff line change
@@ -475,11 +475,13 @@ async def _asyncwith(c):
475475
BEFORE_ASYNC_WITH
476476
GET_AWAITABLE 1
477477
LOAD_CONST 0 (None)
478-
>> SEND 3 (to 22)
478+
>> SEND 3 (to 24)
479479
YIELD_VALUE 2
480480
RESUME 3
481-
JUMP_BACKWARD_NO_INTERRUPT 4 (to 14)
482-
>> POP_TOP
481+
JUMP_BACKWARD_NO_INTERRUPT 5 (to 14)
482+
>> SWAP 2
483+
POP_TOP
484+
POP_TOP
483485
484486
%3d LOAD_CONST 1 (1)
485487
STORE_FAST 1 (x)
@@ -490,30 +492,33 @@ async def _asyncwith(c):
490492
CALL 2
491493
GET_AWAITABLE 2
492494
LOAD_CONST 0 (None)
493-
>> SEND 3 (to 56)
495+
>> SEND 3 (to 64)
494496
YIELD_VALUE 2
495497
RESUME 3
496-
JUMP_BACKWARD_NO_INTERRUPT 4 (to 48)
498+
JUMP_BACKWARD_NO_INTERRUPT 5 (to 54)
497499
>> POP_TOP
500+
POP_TOP
498501
499502
%3d LOAD_CONST 2 (2)
500503
STORE_FAST 2 (y)
501504
RETURN_CONST 0 (None)
502505
503506
%3d >> CLEANUP_THROW
504-
JUMP_BACKWARD 23 (to 22)
507+
JUMP_BACKWARD 27 (to 24)
505508
>> CLEANUP_THROW
506-
JUMP_BACKWARD 8 (to 56)
509+
JUMP_BACKWARD 9 (to 64)
507510
>> PUSH_EXC_INFO
508511
WITH_EXCEPT_START
509512
GET_AWAITABLE 2
510513
LOAD_CONST 0 (None)
511-
>> SEND 4 (to 90)
514+
>> SEND 4 (to 102)
512515
YIELD_VALUE 3
513516
RESUME 3
514-
JUMP_BACKWARD_NO_INTERRUPT 4 (to 80)
517+
JUMP_BACKWARD_NO_INTERRUPT 5 (to 90)
515518
>> CLEANUP_THROW
516-
>> POP_JUMP_IF_TRUE 1 (to 94)
519+
>> SWAP 2
520+
POP_TOP
521+
POP_JUMP_IF_TRUE 1 (to 110)
517522
RERAISE 2
518523
>> POP_TOP
519524
POP_EXCEPT
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Change the ``SEND`` instruction to leave the receiver on the stack. This
2+
allows the specialized form of ``SEND`` to skip the chain of C calls and jump
3+
directly to the ``RESUME`` in the generator or coroutine.

Objects/frameobject.c

+3-3
Original file line numberDiff line numberDiff line change
@@ -334,10 +334,10 @@ mark_stacks(PyCodeObject *code_obj, int len)
334334
break;
335335
}
336336
case SEND:
337-
j = get_arg(code, i) + i + 1;
337+
j = get_arg(code, i) + i + INLINE_CACHE_ENTRIES_SEND + 1;
338338
assert(j < len);
339-
assert(stacks[j] == UNINITIALIZED || stacks[j] == pop_value(next_stack));
340-
stacks[j] = pop_value(next_stack);
339+
assert(stacks[j] == UNINITIALIZED || stacks[j] == next_stack);
340+
stacks[j] = next_stack;
341341
stacks[i+1] = next_stack;
342342
break;
343343
case JUMP_FORWARD:

Python/bytecodes.c

+52-36
Original file line numberDiff line numberDiff line change
@@ -680,51 +680,66 @@ dummy_func(
680680
PREDICT(LOAD_CONST);
681681
}
682682

683-
inst(SEND, (receiver, v -- receiver if (!jump), retval)) {
683+
family(for_iter, INLINE_CACHE_ENTRIES_FOR_ITER) = {
684+
SEND,
685+
SEND_GEN,
686+
};
687+
688+
inst(SEND, (unused/1, receiver, v -- receiver, retval)) {
689+
#if ENABLE_SPECIALIZATION
690+
_PySendCache *cache = (_PySendCache *)next_instr;
691+
if (ADAPTIVE_COUNTER_IS_ZERO(cache->counter)) {
692+
assert(cframe.use_tracing == 0);
693+
next_instr--;
694+
_Py_Specialize_Send(receiver, next_instr);
695+
DISPATCH_SAME_OPARG();
696+
}
697+
STAT_INC(SEND, deferred);
698+
DECREMENT_ADAPTIVE_COUNTER(cache->counter);
699+
#endif /* ENABLE_SPECIALIZATION */
684700
assert(frame != &entry_frame);
685-
bool jump = false;
686-
PySendResult gen_status;
687-
if (tstate->c_tracefunc == NULL) {
688-
gen_status = PyIter_Send(receiver, v, &retval);
689-
} else {
690-
if (Py_IsNone(v) && PyIter_Check(receiver)) {
691-
retval = Py_TYPE(receiver)->tp_iternext(receiver);
692-
}
693-
else {
694-
retval = PyObject_CallMethodOneArg(receiver, &_Py_ID(send), v);
695-
}
696-
if (retval == NULL) {
697-
if (tstate->c_tracefunc != NULL
698-
&& _PyErr_ExceptionMatches(tstate, PyExc_StopIteration))
699-
call_exc_trace(tstate->c_tracefunc, tstate->c_traceobj, tstate, frame);
700-
if (_PyGen_FetchStopIterationValue(&retval) == 0) {
701-
gen_status = PYGEN_RETURN;
702-
}
703-
else {
704-
gen_status = PYGEN_ERROR;
705-
}
701+
if (Py_IsNone(v) && PyIter_Check(receiver)) {
702+
retval = Py_TYPE(receiver)->tp_iternext(receiver);
703+
}
704+
else {
705+
retval = PyObject_CallMethodOneArg(receiver, &_Py_ID(send), v);
706+
}
707+
if (retval == NULL) {
708+
if (tstate->c_tracefunc != NULL
709+
&& _PyErr_ExceptionMatches(tstate, PyExc_StopIteration))
710+
call_exc_trace(tstate->c_tracefunc, tstate->c_traceobj, tstate, frame);
711+
if (_PyGen_FetchStopIterationValue(&retval) == 0) {
712+
assert(retval != NULL);
713+
JUMPBY(oparg);
706714
}
707715
else {
708-
gen_status = PYGEN_NEXT;
716+
assert(retval == NULL);
717+
goto error;
709718
}
710719
}
711-
if (gen_status == PYGEN_ERROR) {
712-
assert(retval == NULL);
713-
goto error;
714-
}
715-
Py_DECREF(v);
716-
if (gen_status == PYGEN_RETURN) {
717-
assert(retval != NULL);
718-
Py_DECREF(receiver);
719-
JUMPBY(oparg);
720-
jump = true;
721-
}
722720
else {
723-
assert(gen_status == PYGEN_NEXT);
724721
assert(retval != NULL);
725722
}
726723
}
727724

725+
inst(SEND_GEN, (unused/1, receiver, v -- receiver)) {
726+
assert(cframe.use_tracing == 0);
727+
PyGenObject *gen = (PyGenObject *)receiver;
728+
DEOPT_IF(Py_TYPE(gen) != &PyGen_Type &&
729+
Py_TYPE(gen) != &PyCoro_Type, SEND);
730+
DEOPT_IF(gen->gi_frame_state >= FRAME_EXECUTING, SEND);
731+
STAT_INC(SEND, hit);
732+
_PyInterpreterFrame *gen_frame = (_PyInterpreterFrame *)gen->gi_iframe;
733+
frame->yield_offset = oparg;
734+
STACK_SHRINK(1);
735+
_PyFrame_StackPush(gen_frame, v);
736+
gen->gi_frame_state = FRAME_EXECUTING;
737+
gen->gi_exc_state.previous_item = tstate->exc_info;
738+
tstate->exc_info = &gen->gi_exc_state;
739+
JUMPBY(INLINE_CACHE_ENTRIES_SEND + oparg);
740+
DISPATCH_INLINED(gen_frame);
741+
}
742+
728743
inst(YIELD_VALUE, (retval -- unused)) {
729744
// NOTE: It's important that YIELD_VALUE never raises an exception!
730745
// The compiler treats any exception raised here as a failed close()
@@ -796,12 +811,13 @@ dummy_func(
796811
}
797812
}
798813

799-
inst(CLEANUP_THROW, (sub_iter, last_sent_val, exc_value -- value)) {
814+
inst(CLEANUP_THROW, (sub_iter, last_sent_val, exc_value -- none, value)) {
800815
assert(throwflag);
801816
assert(exc_value && PyExceptionInstance_Check(exc_value));
802817
if (PyErr_GivenExceptionMatches(exc_value, PyExc_StopIteration)) {
803818
value = Py_NewRef(((PyStopIterationObject *)exc_value)->value);
804819
DECREF_INPUTS();
820+
none = Py_NewRef(Py_None);
805821
}
806822
else {
807823
_PyErr_SetRaisedException(tstate, Py_NewRef(exc_value));

Python/compile.c

+2
Original file line numberDiff line numberDiff line change
@@ -1789,6 +1789,8 @@ compiler_add_yield_from(struct compiler *c, location loc, int await)
17891789
ADDOP(c, loc, CLEANUP_THROW);
17901790

17911791
USE_LABEL(c, exit);
1792+
ADDOP_I(c, loc, SWAP, 2);
1793+
ADDOP(c, loc, POP_TOP);
17921794
return SUCCESS;
17931795
}
17941796

0 commit comments

Comments
 (0)