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.
This commit is contained in:
Vladimir Matveev 2020-09-18 18:38:38 -07:00 committed by GitHub
parent ec8a15b034
commit 2b05361bf7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 148 additions and 39 deletions

View File

@ -15,6 +15,11 @@ than explicitly calling :c:func:`PyGen_New` or :c:func:`PyGen_NewWithQualName`.
The C structure used for generator objects. The C structure used for generator objects.
.. c:type:: PySendResult
The enum value used to represent different results of :c:func:`PyGen_Send`.
.. c:var:: PyTypeObject PyGen_Type .. c:var:: PyTypeObject PyGen_Type
The type object corresponding to generator objects. The type object corresponding to generator objects.
@ -42,3 +47,13 @@ than explicitly calling :c:func:`PyGen_New` or :c:func:`PyGen_NewWithQualName`.
with ``__name__`` and ``__qualname__`` set to *name* and *qualname*. with ``__name__`` and ``__qualname__`` set to *name* and *qualname*.
A reference to *frame* is stolen by this function. The *frame* argument A reference to *frame* is stolen by this function. The *frame* argument
must not be ``NULL``. must not be ``NULL``.
.. c:function:: PySendResult PyGen_Send(PyGenObject *gen, PyObject *arg, PyObject **presult)
Sends the *arg* value into the generator *gen*. Coroutine objects
are also allowed to be as the *gen* argument but they need to be
explicitly casted to PyGenObject*. Returns:
- ``PYGEN_RETURN`` if generator returns. Return value is returned via *presult*.
- ``PYGEN_NEXT`` if generator yields. Yielded value is returned via *presult*.
- ``PYGEN_ERROR`` if generator has raised and exception. *presult* is set to ``NULL``.

View File

@ -959,6 +959,11 @@ PyGen_NewWithQualName:PyFrameObject*:frame:0:
PyGen_NewWithQualName:PyObject*:name:0: PyGen_NewWithQualName:PyObject*:name:0:
PyGen_NewWithQualName:PyObject*:qualname:0: PyGen_NewWithQualName:PyObject*:qualname:0:
PyGen_Send:int:::
PyGen_Send:PyGenObject*:gen:0:
PyGen_Send:PyObject*:arg:0:
PyGen_Send:PyObject**:presult:+1:
PyCoro_CheckExact:int::: PyCoro_CheckExact:int:::
PyCoro_CheckExact:PyObject*:ob:0: PyCoro_CheckExact:PyObject*:ob:0:

View File

@ -45,6 +45,21 @@ PyAPI_FUNC(PyObject *) _PyGen_Send(PyGenObject *, PyObject *);
PyObject *_PyGen_yf(PyGenObject *); PyObject *_PyGen_yf(PyGenObject *);
PyAPI_FUNC(void) _PyGen_Finalize(PyObject *self); PyAPI_FUNC(void) _PyGen_Finalize(PyObject *self);
typedef enum {
PYGEN_RETURN = 0,
PYGEN_ERROR = -1,
PYGEN_NEXT = 1,
} PySendResult;
/* Sends the value into the generator or the coroutine. Returns:
- PYGEN_RETURN (0) if generator has returned.
'result' parameter is filled with return value
- PYGEN_ERROR (-1) if exception was raised.
'result' parameter is NULL
- PYGEN_NEXT (1) if generator has yielded.
'result' parameter is filled with yielded value. */
PyAPI_FUNC(PySendResult) PyGen_Send(PyGenObject *, PyObject *, PyObject **);
#ifndef Py_LIMITED_API #ifndef Py_LIMITED_API
typedef struct { typedef struct {
_PyGenObject_HEAD(cr) _PyGenObject_HEAD(cr)

View File

@ -0,0 +1,2 @@
Add PyGen_Send function to allow sending value into generator/coroutine
without raising StopIteration exception to signal return

View File

@ -2621,6 +2621,20 @@ task_set_error_soon(TaskObj *task, PyObject *et, const char *format, ...)
Py_RETURN_NONE; Py_RETURN_NONE;
} }
static inline int
gen_status_from_result(PyObject **result)
{
if (*result != NULL) {
return PYGEN_NEXT;
}
if (_PyGen_FetchStopIterationValue(result) == 0) {
return PYGEN_RETURN;
}
assert(PyErr_Occurred());
return PYGEN_ERROR;
}
static PyObject * static PyObject *
task_step_impl(TaskObj *task, PyObject *exc) task_step_impl(TaskObj *task, PyObject *exc)
{ {
@ -2679,26 +2693,29 @@ task_step_impl(TaskObj *task, PyObject *exc)
return NULL; return NULL;
} }
int gen_status = PYGEN_ERROR;
if (exc == NULL) { if (exc == NULL) {
if (PyGen_CheckExact(coro) || PyCoro_CheckExact(coro)) { if (PyGen_CheckExact(coro) || PyCoro_CheckExact(coro)) {
result = _PyGen_Send((PyGenObject*)coro, Py_None); gen_status = PyGen_Send((PyGenObject*)coro, Py_None, &result);
} }
else { else {
result = _PyObject_CallMethodIdOneArg(coro, &PyId_send, Py_None); result = _PyObject_CallMethodIdOneArg(coro, &PyId_send, Py_None);
gen_status = gen_status_from_result(&result);
} }
} }
else { else {
result = _PyObject_CallMethodIdOneArg(coro, &PyId_throw, exc); result = _PyObject_CallMethodIdOneArg(coro, &PyId_throw, exc);
gen_status = gen_status_from_result(&result);
if (clear_exc) { if (clear_exc) {
/* We created 'exc' during this call */ /* We created 'exc' during this call */
Py_DECREF(exc); Py_DECREF(exc);
} }
} }
if (result == NULL) { if (gen_status == PYGEN_RETURN || gen_status == PYGEN_ERROR) {
PyObject *et, *ev, *tb; PyObject *et, *ev, *tb;
if (_PyGen_FetchStopIterationValue(&o) == 0) { if (result != NULL) {
/* The error is StopIteration and that means that /* The error is StopIteration and that means that
the underlying coroutine has resolved */ the underlying coroutine has resolved */
@ -2709,10 +2726,10 @@ task_step_impl(TaskObj *task, PyObject *exc)
res = future_cancel((FutureObj*)task, task->task_cancel_msg); res = future_cancel((FutureObj*)task, task->task_cancel_msg);
} }
else { else {
res = future_set_result((FutureObj*)task, o); res = future_set_result((FutureObj*)task, result);
} }
Py_DECREF(o); Py_DECREF(result);
if (res == NULL) { if (res == NULL) {
return NULL; return NULL;

View File

@ -137,7 +137,7 @@ gen_dealloc(PyGenObject *gen)
} }
static PyObject * static PyObject *
gen_send_ex(PyGenObject *gen, PyObject *arg, int exc, int closing) gen_send_ex(PyGenObject *gen, PyObject *arg, int exc, int closing, int *is_return_value)
{ {
PyThreadState *tstate = _PyThreadState_GET(); PyThreadState *tstate = _PyThreadState_GET();
PyFrameObject *f = gen->gi_frame; PyFrameObject *f = gen->gi_frame;
@ -170,6 +170,10 @@ gen_send_ex(PyGenObject *gen, PyObject *arg, int exc, int closing)
PyErr_SetNone(PyExc_StopAsyncIteration); PyErr_SetNone(PyExc_StopAsyncIteration);
} }
else { else {
if (is_return_value != NULL) {
*is_return_value = 1;
Py_RETURN_NONE;
}
PyErr_SetNone(PyExc_StopIteration); PyErr_SetNone(PyExc_StopIteration);
} }
} }
@ -230,18 +234,33 @@ gen_send_ex(PyGenObject *gen, PyObject *arg, int exc, int closing)
/* Delay exception instantiation if we can */ /* Delay exception instantiation if we can */
if (PyAsyncGen_CheckExact(gen)) { if (PyAsyncGen_CheckExact(gen)) {
PyErr_SetNone(PyExc_StopAsyncIteration); PyErr_SetNone(PyExc_StopAsyncIteration);
Py_CLEAR(result);
} }
else if (arg) { else if (arg) {
/* Set exception if not called by gen_iternext() */ if (is_return_value != NULL) {
PyErr_SetNone(PyExc_StopIteration); *is_return_value = 1;
}
else {
/* Set exception if not called by gen_iternext() */
PyErr_SetNone(PyExc_StopIteration);
Py_CLEAR(result);
}
}
else {
Py_CLEAR(result);
} }
} }
else { else {
/* Async generators cannot return anything but None */ /* Async generators cannot return anything but None */
assert(!PyAsyncGen_CheckExact(gen)); assert(!PyAsyncGen_CheckExact(gen));
_PyGen_SetStopIterationValue(result); if (is_return_value != NULL) {
*is_return_value = 1;
}
else {
_PyGen_SetStopIterationValue(result);
Py_CLEAR(result);
}
} }
Py_CLEAR(result);
} }
else if (!result && PyErr_ExceptionMatches(PyExc_StopIteration)) { else if (!result && PyErr_ExceptionMatches(PyExc_StopIteration)) {
const char *msg = "generator raised StopIteration"; const char *msg = "generator raised StopIteration";
@ -264,7 +283,7 @@ gen_send_ex(PyGenObject *gen, PyObject *arg, int exc, int closing)
_PyErr_FormatFromCause(PyExc_RuntimeError, "%s", msg); _PyErr_FormatFromCause(PyExc_RuntimeError, "%s", msg);
} }
if (!result || _PyFrameHasCompleted(f)) { if ((is_return_value && *is_return_value) || !result || _PyFrameHasCompleted(f)) {
/* generator can't be rerun, so release the frame */ /* generator can't be rerun, so release the frame */
/* first clean reference cycle through stored exception traceback */ /* first clean reference cycle through stored exception traceback */
_PyErr_ClearExcState(&gen->gi_exc_state); _PyErr_ClearExcState(&gen->gi_exc_state);
@ -283,7 +302,19 @@ return next yielded value or raise StopIteration.");
PyObject * PyObject *
_PyGen_Send(PyGenObject *gen, PyObject *arg) _PyGen_Send(PyGenObject *gen, PyObject *arg)
{ {
return gen_send_ex(gen, arg, 0, 0); return gen_send_ex(gen, arg, 0, 0, NULL);
}
PySendResult
PyGen_Send(PyGenObject *gen, PyObject *arg, PyObject **result)
{
assert(result != NULL);
int is_return_value = 0;
if ((*result = gen_send_ex(gen, arg, 0, 0, &is_return_value)) == NULL) {
return PYGEN_ERROR;
}
return is_return_value ? PYGEN_RETURN : PYGEN_NEXT;
} }
PyDoc_STRVAR(close_doc, PyDoc_STRVAR(close_doc,
@ -365,7 +396,7 @@ gen_close(PyGenObject *gen, PyObject *args)
} }
if (err == 0) if (err == 0)
PyErr_SetNone(PyExc_GeneratorExit); PyErr_SetNone(PyExc_GeneratorExit);
retval = gen_send_ex(gen, Py_None, 1, 1); retval = gen_send_ex(gen, Py_None, 1, 1, NULL);
if (retval) { if (retval) {
const char *msg = "generator ignored GeneratorExit"; const char *msg = "generator ignored GeneratorExit";
if (PyCoro_CheckExact(gen)) { if (PyCoro_CheckExact(gen)) {
@ -413,7 +444,7 @@ _gen_throw(PyGenObject *gen, int close_on_genexit,
gen->gi_frame->f_state = state; gen->gi_frame->f_state = state;
Py_DECREF(yf); Py_DECREF(yf);
if (err < 0) if (err < 0)
return gen_send_ex(gen, Py_None, 1, 0); return gen_send_ex(gen, Py_None, 1, 0, NULL);
goto throw_here; goto throw_here;
} }
if (PyGen_CheckExact(yf) || PyCoro_CheckExact(yf)) { if (PyGen_CheckExact(yf) || PyCoro_CheckExact(yf)) {
@ -465,10 +496,10 @@ _gen_throw(PyGenObject *gen, int close_on_genexit,
assert(gen->gi_frame->f_lasti >= 0); assert(gen->gi_frame->f_lasti >= 0);
gen->gi_frame->f_lasti += sizeof(_Py_CODEUNIT); gen->gi_frame->f_lasti += sizeof(_Py_CODEUNIT);
if (_PyGen_FetchStopIterationValue(&val) == 0) { if (_PyGen_FetchStopIterationValue(&val) == 0) {
ret = gen_send_ex(gen, val, 0, 0); ret = gen_send_ex(gen, val, 0, 0, NULL);
Py_DECREF(val); Py_DECREF(val);
} else { } else {
ret = gen_send_ex(gen, Py_None, 1, 0); ret = gen_send_ex(gen, Py_None, 1, 0, NULL);
} }
} }
return ret; return ret;
@ -522,7 +553,7 @@ throw_here:
} }
PyErr_Restore(typ, val, tb); PyErr_Restore(typ, val, tb);
return gen_send_ex(gen, Py_None, 1, 0); return gen_send_ex(gen, Py_None, 1, 0, NULL);
failed_throw: failed_throw:
/* Didn't use our arguments, so restore their original refcounts */ /* Didn't use our arguments, so restore their original refcounts */
@ -551,7 +582,7 @@ gen_throw(PyGenObject *gen, PyObject *args)
static PyObject * static PyObject *
gen_iternext(PyGenObject *gen) gen_iternext(PyGenObject *gen)
{ {
return gen_send_ex(gen, NULL, 0, 0); return gen_send_ex(gen, NULL, 0, 0, NULL);
} }
/* /*
@ -1051,13 +1082,13 @@ coro_wrapper_dealloc(PyCoroWrapper *cw)
static PyObject * static PyObject *
coro_wrapper_iternext(PyCoroWrapper *cw) coro_wrapper_iternext(PyCoroWrapper *cw)
{ {
return gen_send_ex((PyGenObject *)cw->cw_coroutine, NULL, 0, 0); return gen_send_ex((PyGenObject *)cw->cw_coroutine, NULL, 0, 0, NULL);
} }
static PyObject * static PyObject *
coro_wrapper_send(PyCoroWrapper *cw, PyObject *arg) coro_wrapper_send(PyCoroWrapper *cw, PyObject *arg)
{ {
return gen_send_ex((PyGenObject *)cw->cw_coroutine, arg, 0, 0); return gen_send_ex((PyGenObject *)cw->cw_coroutine, arg, 0, 0, NULL);
} }
static PyObject * static PyObject *
@ -1570,7 +1601,7 @@ async_gen_asend_send(PyAsyncGenASend *o, PyObject *arg)
} }
o->ags_gen->ag_running_async = 1; o->ags_gen->ag_running_async = 1;
result = gen_send_ex((PyGenObject*)o->ags_gen, arg, 0, 0); result = gen_send_ex((PyGenObject*)o->ags_gen, arg, 0, 0, NULL);
result = async_gen_unwrap_value(o->ags_gen, result); result = async_gen_unwrap_value(o->ags_gen, result);
if (result == NULL) { if (result == NULL) {
@ -1926,7 +1957,7 @@ async_gen_athrow_send(PyAsyncGenAThrow *o, PyObject *arg)
assert(o->agt_state == AWAITABLE_STATE_ITER); assert(o->agt_state == AWAITABLE_STATE_ITER);
retval = gen_send_ex((PyGenObject *)gen, arg, 0, 0); retval = gen_send_ex((PyGenObject *)gen, arg, 0, 0, NULL);
if (o->agt_args) { if (o->agt_args) {
return async_gen_unwrap_value(o->agt_gen, retval); return async_gen_unwrap_value(o->agt_gen, retval);
} else { } else {

View File

@ -2223,29 +2223,53 @@ main_loop:
case TARGET(YIELD_FROM): { case TARGET(YIELD_FROM): {
PyObject *v = POP(); PyObject *v = POP();
PyObject *receiver = TOP(); PyObject *receiver = TOP();
int err; int is_gen_or_coro = PyGen_CheckExact(receiver) || PyCoro_CheckExact(receiver);
if (PyGen_CheckExact(receiver) || PyCoro_CheckExact(receiver)) { int gen_status;
retval = _PyGen_Send((PyGenObject *)receiver, v); if (tstate->c_tracefunc == NULL && is_gen_or_coro) {
gen_status = PyGen_Send((PyGenObject *)receiver, v, &retval);
} else { } else {
_Py_IDENTIFIER(send); if (is_gen_or_coro) {
if (v == Py_None) retval = _PyGen_Send((PyGenObject *)receiver, v);
retval = Py_TYPE(receiver)->tp_iternext(receiver); }
else else {
retval = _PyObject_CallMethodIdOneArg(receiver, &PyId_send, v); _Py_IDENTIFIER(send);
if (v == Py_None) {
retval = Py_TYPE(receiver)->tp_iternext(receiver);
}
else {
retval = _PyObject_CallMethodIdOneArg(receiver, &PyId_send, v);
}
}
if (retval == NULL) {
if (tstate->c_tracefunc != NULL
&& _PyErr_ExceptionMatches(tstate, PyExc_StopIteration))
call_exc_trace(tstate->c_tracefunc, tstate->c_traceobj, tstate, f);
if (_PyGen_FetchStopIterationValue(&retval) == 0) {
gen_status = PYGEN_RETURN;
}
else {
gen_status = PYGEN_ERROR;
}
}
else {
gen_status = PYGEN_NEXT;
}
} }
Py_DECREF(v); Py_DECREF(v);
if (retval == NULL) { if (gen_status == PYGEN_ERROR) {
PyObject *val; assert (retval == NULL);
if (tstate->c_tracefunc != NULL goto error;
&& _PyErr_ExceptionMatches(tstate, PyExc_StopIteration)) }
call_exc_trace(tstate->c_tracefunc, tstate->c_traceobj, tstate, f); if (gen_status == PYGEN_RETURN) {
err = _PyGen_FetchStopIterationValue(&val); assert (retval != NULL);
if (err < 0)
goto error;
Py_DECREF(receiver); Py_DECREF(receiver);
SET_TOP(val); SET_TOP(retval);
retval = NULL;
DISPATCH(); DISPATCH();
} }
assert (gen_status == PYGEN_NEXT);
/* receiver remains on stack, retval is value to be yielded */ /* receiver remains on stack, retval is value to be yielded */
/* and repeat... */ /* and repeat... */
assert(f->f_lasti >= (int)sizeof(_Py_CODEUNIT)); assert(f->f_lasti >= (int)sizeof(_Py_CODEUNIT));