Speedup `_setattr` usage and fix slight performance regressions (#991)

* Speedup `_setattr` usage and fix performance regressions

* Add changelog file

Co-authored-by: Hynek Schlawack <hs@ox.cx>
This commit is contained in:
davfsa 2022-08-07 09:52:28 +02:00 committed by GitHub
parent 983c2c4293
commit a8191556c0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 23 additions and 11 deletions

View File

@ -0,0 +1 @@
Fix slight performance regression in classes with custom ``__setattr__`` and speedup even more.

View File

@ -94,9 +94,9 @@ This is (still) slower than a plain assignment:
-s "import attr; C = attr.make_class('C', ['x', 'y', 'z'], slots=True, frozen=True)" \ -s "import attr; C = attr.make_class('C', ['x', 'y', 'z'], slots=True, frozen=True)" \
"C(1, 2, 3)" "C(1, 2, 3)"
......................................... .........................................
Mean +- std dev: 450 ns +- 26 ns Mean +- std dev: 425 ns +- 16 ns
So on a laptop computer the difference is about 230 nanoseconds (1 second is 1,000,000,000 nanoseconds). So on a laptop computer the difference is about 200 nanoseconds (1 second is 1,000,000,000 nanoseconds).
It's certainly something you'll feel in a hot loop but shouldn't matter in normal code. It's certainly something you'll feel in a hot loop but shouldn't matter in normal code.
Pick what's more important to you. Pick what's more important to you.

View File

@ -915,7 +915,7 @@ class _ClassBuilder:
""" """
Automatically created by attrs. Automatically created by attrs.
""" """
__bound_setattr = _obj_setattr.__get__(self, Attribute) __bound_setattr = _obj_setattr.__get__(self)
for name, value in zip(state_attr_names, state): for name, value in zip(state_attr_names, state):
__bound_setattr(name, value) __bound_setattr(name, value)
@ -2007,6 +2007,7 @@ def _make_init(
cache_hash, cache_hash,
base_attr_map, base_attr_map,
is_exc, is_exc,
needs_cached_setattr,
has_cls_on_setattr, has_cls_on_setattr,
attrs_init, attrs_init,
) )
@ -2019,7 +2020,7 @@ def _make_init(
if needs_cached_setattr: if needs_cached_setattr:
# Save the lookup overhead in __init__ if we need to circumvent # Save the lookup overhead in __init__ if we need to circumvent
# setattr hooks. # setattr hooks.
globs["_setattr"] = _obj_setattr globs["_cached_setattr_get"] = _obj_setattr.__get__
init = _make_method( init = _make_method(
"__attrs_init__" if attrs_init else "__init__", "__attrs_init__" if attrs_init else "__init__",
@ -2036,7 +2037,7 @@ def _setattr(attr_name, value_var, has_on_setattr):
""" """
Use the cached object.setattr to set *attr_name* to *value_var*. Use the cached object.setattr to set *attr_name* to *value_var*.
""" """
return "_setattr(self, '%s', %s)" % (attr_name, value_var) return "_setattr('%s', %s)" % (attr_name, value_var)
def _setattr_with_converter(attr_name, value_var, has_on_setattr): def _setattr_with_converter(attr_name, value_var, has_on_setattr):
@ -2044,7 +2045,7 @@ def _setattr_with_converter(attr_name, value_var, has_on_setattr):
Use the cached object.setattr to set *attr_name* to *value_var*, but run Use the cached object.setattr to set *attr_name* to *value_var*, but run
its converter first. its converter first.
""" """
return "_setattr(self, '%s', %s(%s))" % ( return "_setattr('%s', %s(%s))" % (
attr_name, attr_name,
_init_converter_pat % (attr_name,), _init_converter_pat % (attr_name,),
value_var, value_var,
@ -2086,6 +2087,7 @@ def _attrs_to_init_script(
cache_hash, cache_hash,
base_attr_map, base_attr_map,
is_exc, is_exc,
needs_cached_setattr,
has_cls_on_setattr, has_cls_on_setattr,
attrs_init, attrs_init,
): ):
@ -2101,6 +2103,14 @@ def _attrs_to_init_script(
if pre_init: if pre_init:
lines.append("self.__attrs_pre_init__()") lines.append("self.__attrs_pre_init__()")
if needs_cached_setattr:
lines.append(
# Circumvent the __setattr__ descriptor to save one lookup per
# assignment.
# Note _setattr will be used again below if cache_hash is True
"_setattr = _cached_setattr_get(self)"
)
if frozen is True: if frozen is True:
if slots is True: if slots is True:
fmt_setter = _setattr fmt_setter = _setattr
@ -2315,7 +2325,7 @@ def _attrs_to_init_script(
if frozen: if frozen:
if slots: if slots:
# if frozen and slots, then _setattr defined above # if frozen and slots, then _setattr defined above
init_hash_cache = "_setattr(self, '%s', %s)" init_hash_cache = "_setattr('%s', %s)"
else: else:
# if frozen and not slots, then _inst_dict defined above # if frozen and not slots, then _inst_dict defined above
init_hash_cache = "_inst_dict['%s'] = %s" init_hash_cache = "_inst_dict['%s'] = %s"
@ -2428,7 +2438,7 @@ class Attribute:
) )
# Cache this descriptor here to speed things up later. # Cache this descriptor here to speed things up later.
bound_setattr = _obj_setattr.__get__(self, Attribute) bound_setattr = _obj_setattr.__get__(self)
# Despite the big red warning, people *do* instantiate `Attribute` # Despite the big red warning, people *do* instantiate `Attribute`
# themselves. # themselves.
@ -2525,7 +2535,7 @@ class Attribute:
self._setattrs(zip(self.__slots__, state)) self._setattrs(zip(self.__slots__, state))
def _setattrs(self, name_values_pairs): def _setattrs(self, name_values_pairs):
bound_setattr = _obj_setattr.__get__(self, Attribute) bound_setattr = _obj_setattr.__get__(self)
for name, value in name_values_pairs: for name, value in name_values_pairs:
if name != "metadata": if name != "metadata":
bound_setattr(name, value) bound_setattr(name, value)

View File

@ -745,6 +745,7 @@ class TestFunctional:
src = inspect.getsource(D.__init__) src = inspect.getsource(D.__init__)
assert "_setattr(self, 'x', x)" in src assert "_setattr = _cached_setattr_get(self)" in src
assert "_setattr(self, 'y', y)" in src assert "_setattr('x', x)" in src
assert "_setattr('y', y)" in src
assert object.__setattr__ != D.__setattr__ assert object.__setattr__ != D.__setattr__