[Python-Dev] performance of generator termination (was: Re: problem with recursive "yield from" delegation)

Stefan Behnel stefan_ml at behnel.de
Fri Mar 9 22:25:57 CET 2012


Antoine Pitrou, 08.03.2012 21:36:
> On Thu, 8 Mar 2012 14:36:06 -0600
> Benjamin Peterson wrote:
>> 2012/3/8 Stefan Behnel:
>>> Would that be acceptable for CPython as well or would you prefer full
>>> fledged normalisation?
>>
>> I think we have to normalize for correctness. Consider that it may be
>> some StopIteration subclass which set "value" on construction.
> 
> Perhaps it would be time to drop the whole delayed normalization thing,
> provided the benchmarks don't exhibit a slowdown?

At least for Cython, always normalising the exception can make quite a
difference. For the nqueens benchmark, which uses short running generator
expressions (not affected by this particular change), a quick hack to
fetch, normalise and restore the StopIteration exception raised at the end
of the generator expression run reduces the performance by 10% for me. I'd
expect code similar to the group item iterator in itertools.groupby() to
suffer even worse for very small groups.

A while ago, I wouldn't have expected generator termination to have that an
impact, but when we dropped the single frame+traceback creation at the end
of the generator run in Cython, that boosted the performance of the
compiled nqueens benchmark by 70% and a compiled Python version of
itertools.groupby() ran twice as fast as before. These things can make an
impressively large difference.

http://thread.gmane.org/gmane.comp.python.cython.devel/12993/focus=13044

I'm using the following in Cython now. Note how complex the pre-3.3 case
is, I'm sure that makes it even more worth the special case in older
CPython versions (including 2.x).

"""
static int __Pyx_PyGen_FetchStopIterationValue(PyObject **pvalue) {
    PyObject *et, *ev, *tb;
    PyObject *value = NULL;

    if (PyErr_ExceptionMatches(PyExc_StopIteration)) {
        PyErr_Fetch(&et, &ev, &tb);
        // most common case: plain StopIteration without argument
        if (et == PyExc_StopIteration) {
            if (!ev || !PyObject_IsInstance(ev, PyExc_StopIteration)) {
                // PyErr_SetObject() puts the value directly into ev
                if (!ev) {
                    Py_INCREF(Py_None);
                    ev = Py_None;
                }
                Py_XDECREF(tb);
                Py_DECREF(et);
                *pvalue = ev;
                return 0;
            }
        }
        // otherwise: normalise and check what that gives us
        PyErr_NormalizeException(&et, &ev, &tb);
        if (PyObject_IsInstance(ev, PyExc_StopIteration)) {
            Py_XDECREF(tb);
            Py_DECREF(et);
#if PY_VERSION_HEX >= 0x030300A0
            value = ((PyStopIterationObject *)ev)->value;
            Py_INCREF(value);
            Py_DECREF(ev);
#else
            PyObject* args = PyObject_GetAttrString(ev, "args");
            Py_DECREF(ev);
            if (args) {
                value = PyObject_GetItem(args, 0);
                Py_DECREF(args);
            }
            if (!value)
                PyErr_Clear();
#endif
        } else {
            // looks like normalisation failed - raise the new exception
            PyErr_Restore(et, ev, tb);
            return -1;
        }
    } else if (PyErr_Occurred()) {
        return -1;
    }
    if (value == NULL) {
        Py_INCREF(Py_None);
        value = Py_None;
    }
    *pvalue = value;
    return 0;
}
"""

Stefan



More information about the Python-Dev mailing list