bpo-32492: 1.6x speed up in namedtuple attribute access using C fast-path (#10495)

* bpo-32492: 2.5x speed up in namedtuple attribute access using C fast path

* Add News entry

* fixup! bpo-32492: 2.5x speed up in namedtuple attribute access using C fast path

* Check for tuple in the __get__ of the new descriptor and don't cache the descriptor itself

* Don't inherit from property. Implement GC methods to handle __doc__

* Add a test for the docstring substitution in descriptors

* Update NEWS entry to reflect time against 3.7 branch

* Simplify implementation with argument clinic, better error messages, only __new__

* Use positional-only parameters for the __new__

* Use PyTuple_GET_SIZE and PyTuple_GET_ITEM to tighter the implementation of tuplegetterdescr_get

* Implement __set__ to make tuplegetter a data descriptor

* Use Py_INCREF now that we inline PyTuple_GetItem

* Apply the valid_index() function, saving one test

* Move Py_None test out of the critical path.
This commit is contained in:
Pablo Galindo 2018-12-30 09:24:03 +00:00 committed by Raymond Hettinger
parent b0a6196ffd
commit 3f5fc70c62
5 changed files with 211 additions and 4 deletions

View File

@ -311,6 +311,11 @@ except ImportError:
### namedtuple
################################################################################
try:
from _collections import _tuplegetter
except ImportError:
_tuplegetter = lambda index, doc: property(_itemgetter(index), doc=doc)
_nt_itemgetters = {}
def namedtuple(typename, field_names, *, rename=False, defaults=None, module=None):
@ -454,12 +459,13 @@ def namedtuple(typename, field_names, *, rename=False, defaults=None, module=Non
cache = _nt_itemgetters
for index, name in enumerate(field_names):
try:
itemgetter_object, doc = cache[index]
doc = cache[index]
except KeyError:
itemgetter_object = _itemgetter(index)
doc = f'Alias for field number {index}'
cache[index] = itemgetter_object, doc
class_namespace[name] = property(itemgetter_object, doc=doc)
cache[index] = doc
tuplegetter_object = _tuplegetter(index, doc)
class_namespace[name] = tuplegetter_object
result = type(typename, (tuple,), class_namespace)

View File

@ -514,6 +514,14 @@ class TestNamedTuple(unittest.TestCase):
a.w = 5
self.assertEqual(a.__dict__, {'w': 5})
def test_namedtuple_can_mutate_doc_of_descriptors_independently(self):
A = namedtuple('A', 'x y')
B = namedtuple('B', 'x y')
A.x.__doc__ = 'foo'
B.x.__doc__ = 'bar'
self.assertEqual(A.x.__doc__, 'foo')
self.assertEqual(B.x.__doc__, 'bar')
################################################################################
### Abstract Base Classes

View File

@ -0,0 +1,2 @@
Speed up :class:`namedtuple` attribute access by 1.6x using a C fast-path
for the name descriptors. Patch by Pablo Galindo.

View File

@ -7,6 +7,14 @@
#include <sys/types.h> /* For size_t */
#endif
/*[clinic input]
class _tuplegetter "_tuplegetterobject *" "&tuplegetter_type"
[clinic start generated code]*/
/*[clinic end generated code: output=da39a3ee5e6b4b0d input=ee5ed5baabe35068]*/
static PyTypeObject tuplegetter_type;
#include "clinic/_collectionsmodule.c.h"
/* collections module implementation of a deque() datatype
Written and maintained by Raymond D. Hettinger <python@rcn.com>
*/
@ -2328,6 +2336,156 @@ done:
Py_RETURN_NONE;
}
/* Helper functions for namedtuples */
typedef struct {
PyObject_HEAD
Py_ssize_t index;
PyObject* doc;
} _tuplegetterobject;
/*[clinic input]
@classmethod
_tuplegetter.__new__ as tuplegetter_new
index: Py_ssize_t
doc: object
/
[clinic start generated code]*/
static PyObject *
tuplegetter_new_impl(PyTypeObject *type, Py_ssize_t index, PyObject *doc)
/*[clinic end generated code: output=014be444ad80263f input=87c576a5bdbc0bbb]*/
{
_tuplegetterobject* self;
self = (_tuplegetterobject *)type->tp_alloc(type, 0);
if (self == NULL) {
return NULL;
}
self->index = index;
Py_INCREF(doc);
self->doc = doc;
return (PyObject *)self;
}
static PyObject *
tuplegetterdescr_get(PyObject *self, PyObject *obj, PyObject *type)
{
PyObject *result;
if (obj == NULL) {
Py_INCREF(self);
return self;
}
if (!PyTuple_Check(obj)) {
if (obj == Py_None) {
Py_INCREF(self);
return self;
}
PyErr_Format(PyExc_TypeError,
"descriptor for index '%d' for tuple subclasses "
"doesn't apply to '%s' object",
((_tuplegetterobject*)self)->index,
obj->ob_type->tp_name);
return NULL;
}
Py_ssize_t index = ((_tuplegetterobject*)self)->index;
if (!valid_index(index, PyTuple_GET_SIZE(obj))) {
PyErr_SetString(PyExc_IndexError, "tuple index out of range");
return NULL;
}
result = PyTuple_GET_ITEM(obj, index);
Py_INCREF(result);
return result;
}
static int
tuplegetter_set(PyObject *self, PyObject *obj, PyObject *value)
{
if (value == NULL) {
PyErr_SetString(PyExc_AttributeError, "can't delete attribute");
} else {
PyErr_SetString(PyExc_AttributeError, "can't set attribute");
}
return -1;
}
static int
tuplegetter_traverse(PyObject *self, visitproc visit, void *arg)
{
_tuplegetterobject *tuplegetter = (_tuplegetterobject *)self;
Py_VISIT(tuplegetter->doc);
return 0;
}
static int
tuplegetter_clear(PyObject *self)
{
_tuplegetterobject *tuplegetter = (_tuplegetterobject *)self;
Py_CLEAR(tuplegetter->doc);
return 0;
}
static void
tuplegetter_dealloc(_tuplegetterobject *self)
{
PyObject_GC_UnTrack(self);
tuplegetter_clear((PyObject*)self);
Py_TYPE(self)->tp_free((PyObject*)self);
}
static PyMemberDef tuplegetter_members[] = {
{"__doc__", T_OBJECT, offsetof(_tuplegetterobject, doc), 0},
{0}
};
static PyTypeObject tuplegetter_type = {
PyVarObject_HEAD_INIT(NULL, 0)
"_collections._tuplegetter", /* tp_name */
sizeof(_tuplegetterobject), /* tp_basicsize */
0, /* tp_itemsize */
/* methods */
(destructor)tuplegetter_dealloc, /* tp_dealloc */
0, /* tp_print */
0, /* tp_getattr */
0, /* tp_setattr */
0, /* tp_reserved */
0, /* tp_repr */
0, /* tp_as_number */
0, /* tp_as_sequence */
0, /* tp_as_mapping */
0, /* tp_hash */
0, /* tp_call */
0, /* tp_str */
0, /* tp_getattro */
0, /* tp_setattro */
0, /* tp_as_buffer */
Py_TPFLAGS_DEFAULT | Py_TPFLAGS_HAVE_GC, /* tp_flags */
0, /* tp_doc */
(traverseproc)tuplegetter_traverse, /* tp_traverse */
(inquiry)tuplegetter_clear, /* tp_clear */
0, /* tp_richcompare */
0, /* tp_weaklistoffset */
0, /* tp_iter */
0, /* tp_iternext */
0, /* tp_methods */
tuplegetter_members, /* tp_members */
0, /* tp_getset */
0, /* tp_base */
0, /* tp_dict */
tuplegetterdescr_get, /* tp_descr_get */
tuplegetter_set, /* tp_descr_set */
0, /* tp_dictoffset */
0, /* tp_init */
0, /* tp_alloc */
tuplegetter_new, /* tp_new */
0,
};
/* module level code ********************************************************/
PyDoc_STRVAR(module_doc,
@ -2386,5 +2544,10 @@ PyInit__collections(void)
Py_INCREF(&dequereviter_type);
PyModule_AddObject(m, "_deque_reverse_iterator", (PyObject *)&dequereviter_type);
if (PyType_Ready(&tuplegetter_type) < 0)
return NULL;
Py_INCREF(&tuplegetter_type);
PyModule_AddObject(m, "_tuplegetter", (PyObject *)&tuplegetter_type);
return m;
}

28
Modules/clinic/_collectionsmodule.c.h generated Normal file
View File

@ -0,0 +1,28 @@
/*[clinic input]
preserve
[clinic start generated code]*/
static PyObject *
tuplegetter_new_impl(PyTypeObject *type, Py_ssize_t index, PyObject *doc);
static PyObject *
tuplegetter_new(PyTypeObject *type, PyObject *args, PyObject *kwargs)
{
PyObject *return_value = NULL;
Py_ssize_t index;
PyObject *doc;
if ((type == &tuplegetter_type) &&
!_PyArg_NoKeywords("_tuplegetter", kwargs)) {
goto exit;
}
if (!PyArg_ParseTuple(args, "nO:_tuplegetter",
&index, &doc)) {
goto exit;
}
return_value = tuplegetter_new_impl(type, index, doc);
exit:
return return_value;
}
/*[clinic end generated code: output=83746071eacc28d3 input=a9049054013a1b77]*/