From d49816cd51d6d9c596cc6f9c18c925b87a788960 Mon Sep 17 00:00:00 2001 From: Shawn Yang Date: Sun, 12 Jan 2025 18:13:11 +0800 Subject: [PATCH] perf(python): get object __dict__ for faster field read/write (#2003) ## What does this PR do? This PR extract `__dict__` in object when serialize a dataclass without slots for faster attribute set/set by 16% ## Related issues ## Does this PR introduce any user-facing change? - [ ] Does this PR introduce any public API change? - [ ] Does this PR introduce any binary protocol compatibility change? ## Benchmark For following object: ```python COMPLEX_OBJECT = ComplexObject1( f1=ComplexObject2(f1=True, f2={-1: 2}), f2="abc", f3=["abc", "abc"], f4={1: 2}, f5=2**7 - 1, f6=2**15 - 1, f7=2**31 - 1, f8=2**63 - 1, f9=1.0 / 2, f10=1 / 3.0, f12=[-1, 4], ) ``` This PR gives a 16% speed up --- python/pyfury/serializer.py | 25 +++++++++++++++++++++---- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/python/pyfury/serializer.py b/python/pyfury/serializer.py index 878c456f63..f7276f7d88 100644 --- a/python/pyfury/serializer.py +++ b/python/pyfury/serializer.py @@ -288,6 +288,7 @@ def __init__(self, fury, clz: type): # This will get superclass type hints too. self._type_hints = typing.get_type_hints(clz) self._field_names = sorted(self._type_hints.keys()) + self._has_slots = hasattr(clz, "__slots__") # TODO compute hash self._hash = len(self._field_names) self._generated_write_method = self._gen_write_method() @@ -300,16 +301,21 @@ def __init__(self, fury, clz: type): def _gen_write_method(self): context = {} counter = itertools.count(0) - buffer, fury, value = "buffer", "fury", "value" + buffer, fury, value, value_dict = "buffer", "fury", "value", "value_dict" context[fury] = self.fury stmts = [ f'"""write method for {self.type_}"""', f"{buffer}.write_int32({self._hash})", ] + if not self._has_slots: + stmts.append(f"{value_dict} = {value}.__dict__") for field_name in self._field_names: field_type = self._type_hints[field_name] field_value = f"field_value{next(counter)}" - stmts.append(f"{field_value} = {value}.{field_name}") + if not self._has_slots: + stmts.append(f"{field_value} = {value_dict}['{field_name}']") + else: + stmts.append(f"{field_value} = {value}.{field_name}") if field_type is bool: stmts.extend(gen_write_nullable_basic_stmts(buffer, field_value, bool)) elif field_type == int: @@ -332,7 +338,13 @@ def _gen_write_method(self): def _gen_read_method(self): context = dict(_jit_context) - buffer, fury, obj_class, obj = "buffer", "fury", "obj_class", "obj" + buffer, fury, obj_class, obj, obj_dict = ( + "buffer", + "fury", + "obj_class", + "obj", + "obj_dict", + ) ref_resolver = "ref_resolver" context[fury] = self.fury context[obj_class] = self.type_ @@ -346,9 +358,14 @@ def _gen_read_method(self): f""" raise ClassNotCompatibleError( "Hash read_hash is not consistent with {self._hash} for {self.type_}")""", ] + if not self._has_slots: + stmts.append(f"{obj_dict} = {obj}.__dict__") def set_action(value: str): - return f"{obj}.{field_name} = {value}" + if not self._has_slots: + return f"{obj_dict}['{field_name}'] = {value}" + else: + return f"{obj}.{field_name} = {value}" for field_name in self._field_names: field_type = self._type_hints[field_name]