diff --git a/Include/cpython/code.h b/Include/cpython/code.h index 330f1f54d15203..74619d03bb67b1 100644 --- a/Include/cpython/code.h +++ b/Include/cpython/code.h @@ -35,6 +35,7 @@ struct PyCodeObject { for tracebacks and debuggers; otherwise, constant de-duplication would collapse identical functions/lambdas defined on different lines. */ + PyObject *co_kwarg2index; /* Maps keyword arg names to index in locals */ Py_ssize_t *co_cell2arg; /* Maps cell vars which are arguments. */ PyObject *co_filename; /* unicode (where it was loaded from) */ PyObject *co_name; /* unicode (name, for reference) */ @@ -172,6 +173,8 @@ PyAPI_FUNC(int) _PyCode_GetExtra(PyObject *code, Py_ssize_t index, PyAPI_FUNC(int) _PyCode_SetExtra(PyObject *code, Py_ssize_t index, void *extra); +int _PyCode_InitKwarg2Index(PyCodeObject *co); + /** API for initializing the line number table. */ int _PyCode_InitAddressRange(PyCodeObject* co, PyCodeAddressRange *bounds); diff --git a/Misc/NEWS.d/next/Core and Builtins/2021-05-17-20-46-08.bpo-44160.Z10hI2.rst b/Misc/NEWS.d/next/Core and Builtins/2021-05-17-20-46-08.bpo-44160.Z10hI2.rst new file mode 100644 index 00000000000000..cc043e2625d600 --- /dev/null +++ b/Misc/NEWS.d/next/Core and Builtins/2021-05-17-20-46-08.bpo-44160.Z10hI2.rst @@ -0,0 +1 @@ +Calling with keyword arguments is now linear, rather than quadratic, in the number of keyword arguments. \ No newline at end of file diff --git a/Objects/codeobject.c b/Objects/codeobject.c index e981e39aaf1240..fbcf8e03e4554a 100644 --- a/Objects/codeobject.c +++ b/Objects/codeobject.c @@ -253,6 +253,7 @@ PyCode_NewWithPosOnlyArgs(int argcount, int posonlyargcount, int kwonlyargcount, co->co_freevars = freevars; Py_INCREF(cellvars); co->co_cellvars = cellvars; + co->co_kwarg2index = NULL; // Lazily initialized co->co_cell2arg = cell2arg; Py_INCREF(filename); co->co_filename = filename; @@ -274,6 +275,37 @@ PyCode_NewWithPosOnlyArgs(int argcount, int posonlyargcount, int kwonlyargcount, return co; } +int +_PyCode_InitKwarg2Index(PyCodeObject *co) +{ + assert(co->co_kwarg2index == NULL); + Py_ssize_t total_args = + (Py_ssize_t)co->co_argcount + (Py_ssize_t)co->co_kwonlyargcount; + PyObject *d = PyDict_New(); + if (d == NULL) { + return -1; + } + + for (Py_ssize_t j = co->co_posonlyargcount; j < total_args; j++) { + PyObject *index = PyLong_FromSsize_t(j); + if (index == NULL) { + Py_DECREF(d); + return -1; + } + PyObject *kw = PyTuple_GET_ITEM(co->co_varnames, j); + Py_INCREF(kw); + if (PyDict_SetItem(d, kw, index) < 0) { + Py_DECREF(index); + Py_DECREF(kw); + Py_DECREF(d); + return -1; + } + } + + co->co_kwarg2index = d; + return 0; +} + PyCodeObject * PyCode_New(int argcount, int kwonlyargcount, int nlocals, int stacksize, int flags, @@ -672,6 +704,7 @@ code_dealloc(PyCodeObject *co) Py_XDECREF(co->co_name); Py_XDECREF(co->co_linetable); Py_XDECREF(co->co_exceptiontable); + Py_XDECREF(co->co_kwarg2index); if (co->co_cell2arg != NULL) PyMem_Free(co->co_cell2arg); if (co->co_zombieframe != NULL) diff --git a/Python/ceval.c b/Python/ceval.c index fa7b0b5a051e3a..54f80cc2cbd6c7 100644 --- a/Python/ceval.c +++ b/Python/ceval.c @@ -1,10 +1,5 @@ /* Execute compiled code */ -/* XXX TO DO: - XXX speed up searching for keywords by using a dictionary - XXX document it! - */ - /* enable more aggressive intra-module optimizations, where available */ /* affects both release and debug builds - see bpo-43271 */ #define PY_LOCAL_AGGRESSIVE @@ -262,9 +257,6 @@ UNSIGNAL_ASYNC_EXC(PyInterpreterState *interp) } -#ifdef HAVE_ERRNO_H -#include -#endif #include "ceval_gil.h" void _Py_NO_RETURN @@ -4941,8 +4933,12 @@ _PyEval_MakeFrameVector(PyThreadState *tstate, /* Handle keyword arguments */ if (kwnames != NULL) { Py_ssize_t kwcount = PyTuple_GET_SIZE(kwnames); + if (kwcount > 0 && co->co_kwarg2index == NULL) { + if (_PyCode_InitKwarg2Index(co) < 0) { + goto fail; + } + } for (i = 0; i < kwcount; i++) { - PyObject **co_varnames; PyObject *keyword = PyTuple_GET_ITEM(kwnames, i); PyObject *value = args[i+argcount]; Py_ssize_t j; @@ -4954,42 +4950,29 @@ _PyEval_MakeFrameVector(PyThreadState *tstate, goto fail; } - /* Speed hack: do raw pointer compares. As names are - normally interned this should almost always hit. */ - co_varnames = ((PyTupleObject *)(co->co_varnames))->ob_item; - for (j = co->co_posonlyargcount; j < total_args; j++) { - PyObject *varname = co_varnames[j]; - if (varname == keyword) { - goto kw_found; - } - } - - /* Slow fallback, just in case */ - for (j = co->co_posonlyargcount; j < total_args; j++) { - PyObject *varname = co_varnames[j]; - int cmp = PyObject_RichCompareBool( keyword, varname, Py_EQ); - if (cmp > 0) { - goto kw_found; - } - else if (cmp < 0) { + PyObject *index = PyDict_GetItemWithError(co->co_kwarg2index, keyword); + if (index != NULL) { + j = PyLong_AsSsize_t(index); + if (j == -1 && PyErr_Occurred()) { goto fail; } + goto kw_found; + } + else if (PyErr_Occurred()) { + goto fail; } - assert(j >= total_args); if (kwdict == NULL) { - if (co->co_posonlyargcount && positional_only_passed_as_keyword(tstate, co, kwcount, kwnames, - con->fc_qualname)) + con->fc_qualname)) { goto fail; } - _PyErr_Format(tstate, PyExc_TypeError, - "%U() got an unexpected keyword argument '%S'", - con->fc_qualname, keyword); + "%U() got an unexpected keyword argument '%S'", + con->fc_qualname, keyword); goto fail; }