Commit 618020b0 authored by Stefan Behnel's avatar Stefan Behnel

Use dict version guards to avoid the attribute lookup on cpdef method calls for Python subclasses.

This does not help for classes with "__slots__", nor does it help for alternating calls to different objects.
There is probably still space for improvements, e.g. by using an array of object dict versions.
See #2313.
parent 75efd445
...@@ -13,6 +13,11 @@ Features added ...@@ -13,6 +13,11 @@ Features added
* In CPython 3.6 and later, looking up globals in the module dict is almost * In CPython 3.6 and later, looking up globals in the module dict is almost
as fast as looking up C globals. as fast as looking up C globals.
(Github issue #2313)
* For a Python subclass of an extension type, repeated method calls to non-overridden
cpdef methods can avoid the attribute lookup in Py3.6+, which makes them 4x faster.
(Github issue #2313)
* (In-)equality comparisons of objects to integer literals are faster. * (In-)equality comparisons of objects to integer literals are faster.
(Github issue #2188) (Github issue #2188)
......
...@@ -4341,6 +4341,21 @@ class OverrideCheckNode(StatNode): ...@@ -4341,6 +4341,21 @@ class OverrideCheckNode(StatNode):
code.putln("else if (unlikely((Py_TYPE(%s)->tp_dictoffset != 0)" code.putln("else if (unlikely((Py_TYPE(%s)->tp_dictoffset != 0)"
" || (Py_TYPE(%s)->tp_flags & (Py_TPFLAGS_IS_ABSTRACT | Py_TPFLAGS_HEAPTYPE)))) {" % ( " || (Py_TYPE(%s)->tp_flags & (Py_TPFLAGS_IS_ABSTRACT | Py_TPFLAGS_HEAPTYPE)))) {" % (
self_arg, self_arg)) self_arg, self_arg))
code.putln("#if CYTHON_USE_DICT_VERSIONS && CYTHON_USE_PYTYPE_LOOKUP")
code.putln("static PY_UINT64_T tp_dict_version = 0, obj_dict_version = 0;")
code.putln("if (likely("
"Py_TYPE(%s)->tp_dictoffset && "
"Py_TYPE(%s)->tp_dict && "
"tp_dict_version == __PYX_GET_DICT_VERSION(Py_TYPE(%s)->tp_dict) && "
"obj_dict_version == __PYX_GET_DICT_VERSION(_PyObject_GetDictPtr(%s))"
"));" % (
self_arg, self_arg, self_arg, self_arg))
code.putln("else {")
code.putln("PY_UINT64_T type_dict_guard = (likely(Py_TYPE(%s)->tp_dict)) ? __PYX_GET_DICT_VERSION(Py_TYPE(%s)->tp_dict) : 0;" % (
self_arg, self_arg))
code.putln("#endif")
func_node_temp = code.funcstate.allocate_temp(py_object_type, manage_ref=True) func_node_temp = code.funcstate.allocate_temp(py_object_type, manage_ref=True)
self.func_node.set_cname(func_node_temp) self.func_node.set_cname(func_node_temp)
# need to get attribute manually--scope would return cdef method # need to get attribute manually--scope would return cdef method
...@@ -4350,14 +4365,41 @@ class OverrideCheckNode(StatNode): ...@@ -4350,14 +4365,41 @@ class OverrideCheckNode(StatNode):
code.putln("%s = __Pyx_PyObject_GetAttrStr(%s, %s); %s" % ( code.putln("%s = __Pyx_PyObject_GetAttrStr(%s, %s); %s" % (
func_node_temp, self_arg, interned_attr_cname, err)) func_node_temp, self_arg, interned_attr_cname, err))
code.put_gotref(func_node_temp) code.put_gotref(func_node_temp)
is_builtin_function_or_method = "PyCFunction_Check(%s)" % func_node_temp is_builtin_function_or_method = "PyCFunction_Check(%s)" % func_node_temp
is_overridden = "(PyCFunction_GET_FUNCTION(%s) != (PyCFunction)%s)" % ( is_overridden = "(PyCFunction_GET_FUNCTION(%s) != (PyCFunction)%s)" % (
func_node_temp, self.py_func.entry.func_cname) func_node_temp, self.py_func.entry.func_cname)
code.putln("if (!%s || %s) {" % (is_builtin_function_or_method, is_overridden)) code.putln("if (!%s || %s) {" % (is_builtin_function_or_method, is_overridden))
self.body.generate_execution_code(code) self.body.generate_execution_code(code)
code.putln("}") code.putln("}")
# NOTE: it's not 100% sure that we catch the exact versions here that were used for the lookup,
# but it is very unlikely that the versions change during lookup, and the type dict safe guard
# should increase the chance of detecting such a case.
code.putln("#if CYTHON_USE_DICT_VERSIONS && CYTHON_USE_PYTYPE_LOOKUP")
code.putln("tp_dict_version = likely(Py_TYPE(%s)->tp_dict) ?"
" __PYX_GET_DICT_VERSION(Py_TYPE(%s)->tp_dict) : 0;" % (
self_arg, self_arg))
code.putln("obj_dict_version = likely(Py_TYPE(%s)->tp_dictoffset) ?"
" __PYX_GET_DICT_VERSION(_PyObject_GetDictPtr(%s)) : 0;" % (
self_arg, self_arg))
# Safety check that the type dict didn't change during the lookup. Since CPython looks up the
# attribute (descriptor) first in the type dict and then in the instance dict or through the
# descriptor, the only really far-away lookup when we get here is one in the type dict. So we
# double check the type dict version before and afterwards to guard against later changes of
# the type dict during the lookup process.
code.putln("if (unlikely(type_dict_guard != tp_dict_version)) {")
code.putln("tp_dict_version = obj_dict_version = 0;")
code.putln("}")
code.putln("#endif")
code.put_decref_clear(func_node_temp, PyrexTypes.py_object_type) code.put_decref_clear(func_node_temp, PyrexTypes.py_object_type)
code.funcstate.release_temp(func_node_temp) code.funcstate.release_temp(func_node_temp)
code.putln("#if CYTHON_USE_DICT_VERSIONS && CYTHON_USE_PYTYPE_LOOKUP")
code.putln("}")
code.putln("#endif")
code.putln("}") code.putln("}")
......
# mode: run
# tag: cpdef
# This also makes a nice benchmark for the cpdef method call dispatching code.
cdef class Ext:
"""
>>> x = Ext()
>>> x.rec(10)
0
"""
cpdef rec(self, int i):
return 0 if i < 0 else self.rec(i-1)
class Py(Ext):
"""
>>> p = Py()
>>> p.rec(10)
0
"""
pass
class Slots(Ext):
"""
>>> s = Slots()
>>> s.rec(10)
0
"""
__slots__ = ()
class PyOverride(Ext):
"""
>>> p = PyOverride()
>>> p.rec(10)
5
>>> p.rec(11)
0
"""
def rec(self, i):
return Ext.rec(self, i-1) if i > 10 else 5
class SlotsOverride(Ext):
"""
>>> s = SlotsOverride()
>>> s.rec(10)
6
>>> s.rec(11)
0
"""
__slots__ = ()
def rec(self, i):
return Ext.rec(self, i-1) if i > 10 else 6
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment