Skip to content

Commit 2b05361

Browse files
authored
bpo-41756: Introduce PyGen_Send C API (GH-22196)
The new API allows to efficiently send values into native generators and coroutines avoiding use of StopIteration exceptions to signal returns. ceval loop now uses this method instead of the old "private" _PyGen_Send C API. This translates to 1.6x increased performance of 'await' calls in micro-benchmarks. Aside from CPython core improvements, this new API will also allow Cython to generate more efficient code, benefiting high-performance IO libraries like uvloop.
1 parent ec8a15b commit 2b05361

File tree

7 files changed

+148
-39
lines changed

7 files changed

+148
-39
lines changed

Doc/c-api/gen.rst

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,11 @@ than explicitly calling :c:func:`PyGen_New` or :c:func:`PyGen_NewWithQualName`.
1515
The C structure used for generator objects.
1616

1717

18+
.. c:type:: PySendResult
19+
20+
The enum value used to represent different results of :c:func:`PyGen_Send`.
21+
22+
1823
.. c:var:: PyTypeObject PyGen_Type
1924
2025
The type object corresponding to generator objects.
@@ -42,3 +47,13 @@ than explicitly calling :c:func:`PyGen_New` or :c:func:`PyGen_NewWithQualName`.
4247
with ``__name__`` and ``__qualname__`` set to *name* and *qualname*.
4348
A reference to *frame* is stolen by this function. The *frame* argument
4449
must not be ``NULL``.
50+
51+
.. c:function:: PySendResult PyGen_Send(PyGenObject *gen, PyObject *arg, PyObject **presult)
52+
53+
Sends the *arg* value into the generator *gen*. Coroutine objects
54+
are also allowed to be as the *gen* argument but they need to be
55+
explicitly casted to PyGenObject*. Returns:
56+
57+
- ``PYGEN_RETURN`` if generator returns. Return value is returned via *presult*.
58+
- ``PYGEN_NEXT`` if generator yields. Yielded value is returned via *presult*.
59+
- ``PYGEN_ERROR`` if generator has raised and exception. *presult* is set to ``NULL``.

Doc/data/refcounts.dat

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -959,6 +959,11 @@ PyGen_NewWithQualName:PyFrameObject*:frame:0:
959959
PyGen_NewWithQualName:PyObject*:name:0:
960960
PyGen_NewWithQualName:PyObject*:qualname:0:
961961

962+
PyGen_Send:int:::
963+
PyGen_Send:PyGenObject*:gen:0:
964+
PyGen_Send:PyObject*:arg:0:
965+
PyGen_Send:PyObject**:presult:+1:
966+
962967
PyCoro_CheckExact:int:::
963968
PyCoro_CheckExact:PyObject*:ob:0:
964969

Include/genobject.h

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,21 @@ PyAPI_FUNC(PyObject *) _PyGen_Send(PyGenObject *, PyObject *);
4545
PyObject *_PyGen_yf(PyGenObject *);
4646
PyAPI_FUNC(void) _PyGen_Finalize(PyObject *self);
4747

48+
typedef enum {
49+
PYGEN_RETURN = 0,
50+
PYGEN_ERROR = -1,
51+
PYGEN_NEXT = 1,
52+
} PySendResult;
53+
54+
/* Sends the value into the generator or the coroutine. Returns:
55+
- PYGEN_RETURN (0) if generator has returned.
56+
'result' parameter is filled with return value
57+
- PYGEN_ERROR (-1) if exception was raised.
58+
'result' parameter is NULL
59+
- PYGEN_NEXT (1) if generator has yielded.
60+
'result' parameter is filled with yielded value. */
61+
PyAPI_FUNC(PySendResult) PyGen_Send(PyGenObject *, PyObject *, PyObject **);
62+
4863
#ifndef Py_LIMITED_API
4964
typedef struct {
5065
_PyGenObject_HEAD(cr)
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Add PyGen_Send function to allow sending value into generator/coroutine
2+
without raising StopIteration exception to signal return

Modules/_asynciomodule.c

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2621,6 +2621,20 @@ task_set_error_soon(TaskObj *task, PyObject *et, const char *format, ...)
26212621
Py_RETURN_NONE;
26222622
}
26232623

2624+
static inline int
2625+
gen_status_from_result(PyObject **result)
2626+
{
2627+
if (*result != NULL) {
2628+
return PYGEN_NEXT;
2629+
}
2630+
if (_PyGen_FetchStopIterationValue(result) == 0) {
2631+
return PYGEN_RETURN;
2632+
}
2633+
2634+
assert(PyErr_Occurred());
2635+
return PYGEN_ERROR;
2636+
}
2637+
26242638
static PyObject *
26252639
task_step_impl(TaskObj *task, PyObject *exc)
26262640
{
@@ -2679,26 +2693,29 @@ task_step_impl(TaskObj *task, PyObject *exc)
26792693
return NULL;
26802694
}
26812695

2696+
int gen_status = PYGEN_ERROR;
26822697
if (exc == NULL) {
26832698
if (PyGen_CheckExact(coro) || PyCoro_CheckExact(coro)) {
2684-
result = _PyGen_Send((PyGenObject*)coro, Py_None);
2699+
gen_status = PyGen_Send((PyGenObject*)coro, Py_None, &result);
26852700
}
26862701
else {
26872702
result = _PyObject_CallMethodIdOneArg(coro, &PyId_send, Py_None);
2703+
gen_status = gen_status_from_result(&result);
26882704
}
26892705
}
26902706
else {
26912707
result = _PyObject_CallMethodIdOneArg(coro, &PyId_throw, exc);
2708+
gen_status = gen_status_from_result(&result);
26922709
if (clear_exc) {
26932710
/* We created 'exc' during this call */
26942711
Py_DECREF(exc);
26952712
}
26962713
}
26972714

2698-
if (result == NULL) {
2715+
if (gen_status == PYGEN_RETURN || gen_status == PYGEN_ERROR) {
26992716
PyObject *et, *ev, *tb;
27002717

2701-
if (_PyGen_FetchStopIterationValue(&o) == 0) {
2718+
if (result != NULL) {
27022719
/* The error is StopIteration and that means that
27032720
the underlying coroutine has resolved */
27042721

@@ -2709,10 +2726,10 @@ task_step_impl(TaskObj *task, PyObject *exc)
27092726
res = future_cancel((FutureObj*)task, task->task_cancel_msg);
27102727
}
27112728
else {
2712-
res = future_set_result((FutureObj*)task, o);
2729+
res = future_set_result((FutureObj*)task, result);
27132730
}
27142731

2715-
Py_DECREF(o);
2732+
Py_DECREF(result);
27162733

27172734
if (res == NULL) {
27182735
return NULL;

Objects/genobject.c

Lines changed: 48 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -137,7 +137,7 @@ gen_dealloc(PyGenObject *gen)
137137
}
138138

139139
static PyObject *
140-
gen_send_ex(PyGenObject *gen, PyObject *arg, int exc, int closing)
140+
gen_send_ex(PyGenObject *gen, PyObject *arg, int exc, int closing, int *is_return_value)
141141
{
142142
PyThreadState *tstate = _PyThreadState_GET();
143143
PyFrameObject *f = gen->gi_frame;
@@ -170,6 +170,10 @@ gen_send_ex(PyGenObject *gen, PyObject *arg, int exc, int closing)
170170
PyErr_SetNone(PyExc_StopAsyncIteration);
171171
}
172172
else {
173+
if (is_return_value != NULL) {
174+
*is_return_value = 1;
175+
Py_RETURN_NONE;
176+
}
173177
PyErr_SetNone(PyExc_StopIteration);
174178
}
175179
}
@@ -230,18 +234,33 @@ gen_send_ex(PyGenObject *gen, PyObject *arg, int exc, int closing)
230234
/* Delay exception instantiation if we can */
231235
if (PyAsyncGen_CheckExact(gen)) {
232236
PyErr_SetNone(PyExc_StopAsyncIteration);
237+
Py_CLEAR(result);
233238
}
234239
else if (arg) {
235-
/* Set exception if not called by gen_iternext() */
236-
PyErr_SetNone(PyExc_StopIteration);
240+
if (is_return_value != NULL) {
241+
*is_return_value = 1;
242+
}
243+
else {
244+
/* Set exception if not called by gen_iternext() */
245+
PyErr_SetNone(PyExc_StopIteration);
246+
Py_CLEAR(result);
247+
}
248+
}
249+
else {
250+
Py_CLEAR(result);
237251
}
238252
}
239253
else {
240254
/* Async generators cannot return anything but None */
241255
assert(!PyAsyncGen_CheckExact(gen));
242-
_PyGen_SetStopIterationValue(result);
256+
if (is_return_value != NULL) {
257+
*is_return_value = 1;
258+
}
259+
else {
260+
_PyGen_SetStopIterationValue(result);
261+
Py_CLEAR(result);
262+
}
243263
}
244-
Py_CLEAR(result);
245264
}
246265
else if (!result && PyErr_ExceptionMatches(PyExc_StopIteration)) {
247266
const char *msg = "generator raised StopIteration";
@@ -264,7 +283,7 @@ gen_send_ex(PyGenObject *gen, PyObject *arg, int exc, int closing)
264283
_PyErr_FormatFromCause(PyExc_RuntimeError, "%s", msg);
265284
}
266285

267-
if (!result || _PyFrameHasCompleted(f)) {
286+
if ((is_return_value && *is_return_value) || !result || _PyFrameHasCompleted(f)) {
268287
/* generator can't be rerun, so release the frame */
269288
/* first clean reference cycle through stored exception traceback */
270289
_PyErr_ClearExcState(&gen->gi_exc_state);
@@ -283,7 +302,19 @@ return next yielded value or raise StopIteration.");
283302
PyObject *
284303
_PyGen_Send(PyGenObject *gen, PyObject *arg)
285304
{
286-
return gen_send_ex(gen, arg, 0, 0);
305+
return gen_send_ex(gen, arg, 0, 0, NULL);
306+
}
307+
308+
PySendResult
309+
PyGen_Send(PyGenObject *gen, PyObject *arg, PyObject **result)
310+
{
311+
assert(result != NULL);
312+
313+
int is_return_value = 0;
314+
if ((*result = gen_send_ex(gen, arg, 0, 0, &is_return_value)) == NULL) {
315+
return PYGEN_ERROR;
316+
}
317+
return is_return_value ? PYGEN_RETURN : PYGEN_NEXT;
287318
}
288319

289320
PyDoc_STRVAR(close_doc,
@@ -365,7 +396,7 @@ gen_close(PyGenObject *gen, PyObject *args)
365396
}
366397
if (err == 0)
367398
PyErr_SetNone(PyExc_GeneratorExit);
368-
retval = gen_send_ex(gen, Py_None, 1, 1);
399+
retval = gen_send_ex(gen, Py_None, 1, 1, NULL);
369400
if (retval) {
370401
const char *msg = "generator ignored GeneratorExit";
371402
if (PyCoro_CheckExact(gen)) {
@@ -413,7 +444,7 @@ _gen_throw(PyGenObject *gen, int close_on_genexit,
413444
gen->gi_frame->f_state = state;
414445
Py_DECREF(yf);
415446
if (err < 0)
416-
return gen_send_ex(gen, Py_None, 1, 0);
447+
return gen_send_ex(gen, Py_None, 1, 0, NULL);
417448
goto throw_here;
418449
}
419450
if (PyGen_CheckExact(yf) || PyCoro_CheckExact(yf)) {
@@ -465,10 +496,10 @@ _gen_throw(PyGenObject *gen, int close_on_genexit,
465496
assert(gen->gi_frame->f_lasti >= 0);
466497
gen->gi_frame->f_lasti += sizeof(_Py_CODEUNIT);
467498
if (_PyGen_FetchStopIterationValue(&val) == 0) {
468-
ret = gen_send_ex(gen, val, 0, 0);
499+
ret = gen_send_ex(gen, val, 0, 0, NULL);
469500
Py_DECREF(val);
470501
} else {
471-
ret = gen_send_ex(gen, Py_None, 1, 0);
502+
ret = gen_send_ex(gen, Py_None, 1, 0, NULL);
472503
}
473504
}
474505
return ret;
@@ -522,7 +553,7 @@ _gen_throw(PyGenObject *gen, int close_on_genexit,
522553
}
523554

524555
PyErr_Restore(typ, val, tb);
525-
return gen_send_ex(gen, Py_None, 1, 0);
556+
return gen_send_ex(gen, Py_None, 1, 0, NULL);
526557

527558
failed_throw:
528559
/* Didn't use our arguments, so restore their original refcounts */
@@ -551,7 +582,7 @@ gen_throw(PyGenObject *gen, PyObject *args)
551582
static PyObject *
552583
gen_iternext(PyGenObject *gen)
553584
{
554-
return gen_send_ex(gen, NULL, 0, 0);
585+
return gen_send_ex(gen, NULL, 0, 0, NULL);
555586
}
556587

557588
/*
@@ -1051,13 +1082,13 @@ coro_wrapper_dealloc(PyCoroWrapper *cw)
10511082
static PyObject *
10521083
coro_wrapper_iternext(PyCoroWrapper *cw)
10531084
{
1054-
return gen_send_ex((PyGenObject *)cw->cw_coroutine, NULL, 0, 0);
1085+
return gen_send_ex((PyGenObject *)cw->cw_coroutine, NULL, 0, 0, NULL);
10551086
}
10561087

10571088
static PyObject *
10581089
coro_wrapper_send(PyCoroWrapper *cw, PyObject *arg)
10591090
{
1060-
return gen_send_ex((PyGenObject *)cw->cw_coroutine, arg, 0, 0);
1091+
return gen_send_ex((PyGenObject *)cw->cw_coroutine, arg, 0, 0, NULL);
10611092
}
10621093

10631094
static PyObject *
@@ -1570,7 +1601,7 @@ async_gen_asend_send(PyAsyncGenASend *o, PyObject *arg)
15701601
}
15711602

15721603
o->ags_gen->ag_running_async = 1;
1573-
result = gen_send_ex((PyGenObject*)o->ags_gen, arg, 0, 0);
1604+
result = gen_send_ex((PyGenObject*)o->ags_gen, arg, 0, 0, NULL);
15741605
result = async_gen_unwrap_value(o->ags_gen, result);
15751606

15761607
if (result == NULL) {
@@ -1926,7 +1957,7 @@ async_gen_athrow_send(PyAsyncGenAThrow *o, PyObject *arg)
19261957

19271958
assert(o->agt_state == AWAITABLE_STATE_ITER);
19281959

1929-
retval = gen_send_ex((PyGenObject *)gen, arg, 0, 0);
1960+
retval = gen_send_ex((PyGenObject *)gen, arg, 0, 0, NULL);
19301961
if (o->agt_args) {
19311962
return async_gen_unwrap_value(o->agt_gen, retval);
19321963
} else {

Python/ceval.c

Lines changed: 41 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -2223,29 +2223,53 @@ _PyEval_EvalFrameDefault(PyThreadState *tstate, PyFrameObject *f, int throwflag)
22232223
case TARGET(YIELD_FROM): {
22242224
PyObject *v = POP();
22252225
PyObject *receiver = TOP();
2226-
int err;
2227-
if (PyGen_CheckExact(receiver) || PyCoro_CheckExact(receiver)) {
2228-
retval = _PyGen_Send((PyGenObject *)receiver, v);
2226+
int is_gen_or_coro = PyGen_CheckExact(receiver) || PyCoro_CheckExact(receiver);
2227+
int gen_status;
2228+
if (tstate->c_tracefunc == NULL && is_gen_or_coro) {
2229+
gen_status = PyGen_Send((PyGenObject *)receiver, v, &retval);
22292230
} else {
2230-
_Py_IDENTIFIER(send);
2231-
if (v == Py_None)
2232-
retval = Py_TYPE(receiver)->tp_iternext(receiver);
2233-
else
2234-
retval = _PyObject_CallMethodIdOneArg(receiver, &PyId_send, v);
2231+
if (is_gen_or_coro) {
2232+
retval = _PyGen_Send((PyGenObject *)receiver, v);
2233+
}
2234+
else {
2235+
_Py_IDENTIFIER(send);
2236+
if (v == Py_None) {
2237+
retval = Py_TYPE(receiver)->tp_iternext(receiver);
2238+
}
2239+
else {
2240+
retval = _PyObject_CallMethodIdOneArg(receiver, &PyId_send, v);
2241+
}
2242+
}
2243+
2244+
if (retval == NULL) {
2245+
if (tstate->c_tracefunc != NULL
2246+
&& _PyErr_ExceptionMatches(tstate, PyExc_StopIteration))
2247+
call_exc_trace(tstate->c_tracefunc, tstate->c_traceobj, tstate, f);
2248+
if (_PyGen_FetchStopIterationValue(&retval) == 0) {
2249+
gen_status = PYGEN_RETURN;
2250+
}
2251+
else {
2252+
gen_status = PYGEN_ERROR;
2253+
}
2254+
}
2255+
else {
2256+
gen_status = PYGEN_NEXT;
2257+
}
22352258
}
22362259
Py_DECREF(v);
2237-
if (retval == NULL) {
2238-
PyObject *val;
2239-
if (tstate->c_tracefunc != NULL
2240-
&& _PyErr_ExceptionMatches(tstate, PyExc_StopIteration))
2241-
call_exc_trace(tstate->c_tracefunc, tstate->c_traceobj, tstate, f);
2242-
err = _PyGen_FetchStopIterationValue(&val);
2243-
if (err < 0)
2244-
goto error;
2260+
if (gen_status == PYGEN_ERROR) {
2261+
assert (retval == NULL);
2262+
goto error;
2263+
}
2264+
if (gen_status == PYGEN_RETURN) {
2265+
assert (retval != NULL);
2266+
22452267
Py_DECREF(receiver);
2246-
SET_TOP(val);
2268+
SET_TOP(retval);
2269+
retval = NULL;
22472270
DISPATCH();
22482271
}
2272+
assert (gen_status == PYGEN_NEXT);
22492273
/* receiver remains on stack, retval is value to be yielded */
22502274
/* and repeat... */
22512275
assert(f->f_lasti >= (int)sizeof(_Py_CODEUNIT));

0 commit comments

Comments
 (0)