Skip to content

GH-100930: Implement dataclass code caching - #92650

Closed
brandtbucher wants to merge 20 commits into
python:mainfrom
brandtbucher:dataclasses
Closed

brandtbucher wants to merge 20 commits into
python:mainfrom
brandtbucher:dataclasses

Conversation

@brandtbucher

@brandtbucher brandtbucher commented May 11, 2022 •

Copy link
Copy Markdown
Member

This is a minimal working implementation of "code-caching" for dataclasses. It's heavily inspired by https://github.com/dabeaz/dataklasses, and works by reusing generated code objects for dataclasses that differ only in the names of their fields. "Template" code objects are lazily created with placeholder values (_field_1, _field_3) that are patched at method generation time using their replace method. Annotations and default arguments for __init__ methods are assigned manually, as well.

For microbenchmarks on "simple" dataclasses with 1-10 elements and no "special" fields, class generation now takes a little less than half the time it used to take. The test_dataclasses suite, which contains lots of examples of advanced use-cases and actually does some real work with them, runs about 40% faster vs. main.

I've also included a counter for measuring cache stats. These indicate that when running test_dataclasses, 1,395 methods are generated, but only 91 don't have suitable templates in the code cache yet and need to be constructed using exec. So even for the wide range of dataclasses present in this program, we're still able to maintain a hit rate above 90% (__init__ methods are, predictably, the source of most of the misses).

@brandtbucher brandtbucher added performance Performance or resource usage stdlib Standard Library Python modules in the Lib/ directory 3.12 only security fixes labels May 11, 2022
@brandtbucher
brandtbucher requested a review from ericvsmith May 11, 2022 00:15
@gpshead

gpshead commented May 14, 2022

Copy link
Copy Markdown
Member

Could this be further sped up by having dataclasses.py come with a pre-seeded code cache from inlined code that'd already be part of the .pyc file thus avoiding runtime calls to exec() entirely for things that match its shapes?

@brandtbucher brandtbucher self-assigned this May 16, 2022
@brandtbucher

brandtbucher commented May 20, 2022 •

Copy link
Copy Markdown
Member Author

Could this be further sped up by having dataclasses.py come with a pre-seeded code cache from inlined code that'd already be part of the .pyc file thus avoiding runtime calls to exec() entirely for things that match its shapes?

It could, but I'm not sure I'd want to include that in the initial implementation, mostly because it would be quite a bit of extra code and maintenance burden for a smallish gain that only pays off once (if at all).

What sorts of methods would we pre-generate? __init__, __repr__, and __eq__ for dataclasses with 2 <= n < 6 "simple" members and no defaults, maybe? That's twelve new functions that need to be written and maintained by hand, mostly based on an arbitrary heuristic:

def __init__(self, _field_0, _field_1):
    self._field_0 = _field_0
    self._field_1 = _field_1

_code_cache[<indented function source>] = __init__.__code__

def __repr__(self):
    return self.__class__.__qualname__ + f"(_field_0={self._field_0}, _field_1={self._field_1})"

_code_cache[<indented function source>] = __repr__.__code__

def __eq__(self, other):
    if other.__class__ is self.__class__:
        return (self._field_0, self._field_1) == (other._field_0, other._field_1)
    return NotImplemented

_code_cache[<indented function source>] = __eq__.__code__

<repeat for all other dataclass shapes we want to pre-seed>

Sure, we could rework this to make it more ergonomic:

# XXX: Make sure to regenerate this using
# Tools/scripts/generate_dataclasses_code_cache.py whenever the bytecode or the
# generated methods change!
_MARSHALLED_CODE_CACHE = b"..."
_code_cache = marshal.loads(_MARSHALLED_CODE_CACHE)

But in the end, the pre-seeded cache still runs the risk of falling out of sync with the actual generated methods if they change in the future. If not properly tested and maintained, it could lead to some extremely subtle bugs. So I'm just not sure it's worth it.

@brandtbucher brandtbucher changed the title Implement dataclass code caching GH-100930: Implement dataclass code caching Jan 11, 2023
@brandtbucher
brandtbucher marked this pull request as ready for review January 11, 2023 01:49
Comment thread Lib/dataclasses.py
_code_cache_hits += 1
code = _code_cache[key]
else:
code = _code_cache[key] = _create_code(name, key, globals, locals)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that this will make regtest fail. We have a similar pattern in typing.py: https://github.com/python/cpython/blame/8dd2766d99f8f51ad62dc0fde8282483590c6cd0/Lib/typing.py#L2550-L2603

And we need to call clear_overloads to clear module level cache. Otherwise we get "Env has changed" error.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hm, I'm not sure I understand. I don't see any test failures locally, even when testing for refleaks.

I also only see one use of clear_overloads in the entire code base (as part of a normal test to make sure that calling it works).

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, if refleak test passes, then it is fine :) Thanks for double checking!

@brandtbucher

Copy link
Copy Markdown
Member Author

Closing, since it looks like the direction we're heading right now is batching up exec calls (#109870), rather than caching them.

The code is still here if we decide to do something like this in the future. :)

@charles-cooper

Copy link
Copy Markdown

i've been investigating dataclass performance. per https://discuss.python.org/t/improving-dataclasses-startup-performance/15442/15, would it be possible to just cache the code generated for the dataclass somewhere, and then it gets compiled to pyc like other python files?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

3.12 only security fixes awaiting core review performance Performance or resource usage stdlib Standard Library Python modules in the Lib/ directory

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants