Repository navigation
Dataclasses - Improve the performance of _dataclass_{get,set}state #103032
Copy link
Copy link
Open
Labels
3.13only security fixesonly security fixesperformancePerformance or resource usagePerformance or resource usagestdlibStandard Library Python modules in the Lib/ directoryStandard Library Python modules in the Lib/ directorytopic-dataclassestype-featureA feature request or enhancementA feature request or enhancement
Description
Feature or enhancement
I've noticed this comment yesterday:
Lines 1128 to 1131 in 1fd603f
| # _dataclass_getstate and _dataclass_setstate are needed for pickling frozen | |
| # classes with slots. These could be slightly more performant if we generated | |
| # the code instead of iterating over fields. But that can be a project for | |
| # another day, if performance becomes an issue. |
So, out of curiosity I've decided to try this out. How fast can I make it?
The results are here:
Testing ZeroFields
Dump time (before): 0.229174 sec
Dump time (after) : 0.147407 sec
Testing OneField
Dump time (before): 0.260065 sec
Dump time (after) : 0.197960 sec
Testing EightFields
Dump time (before): 0.331278 sec
Dump time (after) : 0.192171 sec
Testing NestedFields
Dump time (before): 0.846335 sec
Dump time (after) : 0.470739 sec
Here's the simple benchmark that I was using:
import pickle
from dataclasses import dataclass
from timeit import timeit
def perf(e):
print('Testing', e.__class__.__name__)
d = pickle.dumps(e)
iterations = 10000
t1 = timeit(lambda: pickle.dumps(e), number=iterations)
t2 = timeit(lambda: pickle.loads(d), number=iterations)
print(f'Dump time: {t1:.6f} sec')
print(f'Load time: {t2:.6f} sec')
print()
@dataclass(frozen=True, slots=True)
class ZeroFields:
pass
perf(ZeroFields())
@dataclass(frozen=True, slots=True)
class OneField:
foo: str
perf(OneField('l'))
@dataclass(frozen=True, slots=True)
class EightFields:
foo: str
bar: int
baz: int
spam: list[int]
eggs: dict[str, str]
x: bool
y: bool
z: bool
perf(EightFields(
"a", 1, 2, [1, 2, 3, 4, 5], {'a': 'a', 'b': 'b', 'c': 'c'},
True, False, True,
))
@dataclass(frozen=True, slots=True)
class NestedFields:
z1: ZeroFields
z2: ZeroFields
e1: EightFields
e2: EightFields
e = EightFields(
"a", 1, 2, [1, 2, 3, 4, 5], {'a': 'a', 'b': 'b', 'c': 'c'},
True, False, True,
)
perf(NestedFields(ZeroFields(), ZeroFields(), e, e))Here's the very rough version of what I am planning to do:
cls.__getstate__ = _create_fn(
'__getstate__',
['self'],
[f"return ({', '.join(f'self.{f.name}' for f in fields(cls))})"],
)Things to do:
- Refactor current example code to be inline with other code generators
- Add
__setstate__similar support
Does it look like a good enough speed up to make this change?
CC @ericvsmith and @carljm
Reactions are currently unavailable
Metadata
Metadata
Assignees
Labels
3.13only security fixesonly security fixesperformancePerformance or resource usagePerformance or resource usagestdlibStandard Library Python modules in the Lib/ directoryStandard Library Python modules in the Lib/ directorytopic-dataclassestype-featureA feature request or enhancementA feature request or enhancement
Activity
sobolevn commentedon Mar 26, 2023
Final timings:
But, since we now do more work during dataclass creation, I wanted to measure this effect as well. Here's my small benchmark:
Here are the results:
So, we are basically trading "startup time" with "runtime time".
I am not sure which one is more important here.
One more thing, notice
numbers=10000in thepicklebenchmark, but the second one has onlynumbers=100. So, if we care about the absolute time - I guess making the dataclass creation slower is not worth it.Here's my final patch:
and:
Please, share your feedback and ideas.
pochmann commentedon Mar 26, 2023
Maybe an
attrgetterprebuilt at data class creation would have both fast creation and fast application? Benchmark for application, usingEightFields:Note that
attrgetter_on_the_flyincludes creation time, so is only included to show that the creation of theattrgetteris fast. Andattrgetter_prebuilt_wrappedis included because I'm just not sure whetherattrgetter_prebuiltcan be used directly, likecls.__getstate__ = attrgetter(...).Code
Attempt This Online!
sobolevn commentedon Mar 26, 2023
attrgetterseems like an interesting idea. However, it does not work onZeroFields:So, we need to avaluate
map(attrgetter('name'), fields(cls))early and convert it totuple.And have different code paths for empty types and types with fields.
Moreover, simple
cls.__getstate__ = attrgetter(*map(attrgetter('name'), fields(cls)))does not work anyway, it is required to use a wrapper. All in all, this is the final version:And here are the timings:
With only one generated method (we still have to do something similar to
__setstate__) the creation times are:So, as you can see the creation time with only one method is slower that the hardcode patch and dump times are also worth.
Maybe there's something we can improve? But, in the current state is not worth it.
pochmann commentedon Mar 26, 2023
My first instinct is: "Let's fix
attrgetter". Don't you hate it when things refuse to work with empty things...Second instinct: Don't check every time, use a function instead of
None:Can you tell why the wrapper is needed?
That looks odd. Does that mean that creating the attrgetter took negative time?
sobolevn commentedon Mar 26, 2023
I think I've just messed something up while copy-pasting 🤔
Please, let me double check the results.
pochmann commentedon Mar 26, 2023
Actually, without the extra attribute (and calling
fields(cls)only once):carljm commentedon Mar 27, 2023
Every dataclass must be created; many dataclasses are never pickled at all. So I am not super excited about trading creation time for pickling performance (even though the latter could happen many times per class.) Haven't followed all of the numbers or options here (looks like we don't have reliable numbers for the latest attrgetter option(s) yet?) but IMO for this to be worth it we should have very small impact on dataclass creation time.