-
-
Notifications
You must be signed in to change notification settings - Fork 374
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Pull threading.local out of the generated repr's globals. #857
Conversation
@thetorpedodog Could you show us the performance impact of this change on a sample class? I recommend |
Benchmarks from Python 3.9.2 on Debian: Before:
After:
Obviously this means it is 1 nsec (± 22 nsec) faster, a clear and meaningful performance improvement that is not at all probably just noise :) |
(just did a rebase to linearize history across this and #854) |
@thetorpedodog Interesting, because that's not what I'm seeing at all ;)
Your branch:
(CPython 3.9.7 on Ubuntu.) That's a very significant slowdown. My gut feeling was there was no chance the proposed code would be as fast as the code that's in there now since not only are functions calls kinda expensive in CPython, you're using a context manager too. |
Your script shows the same slowdown btw. I suspect you've forgotten to switch the branch between runs. Sorry :) |
TBH I wouldn't consider performance of repr a top priority. Isn't it usually used only in interactive use? My gut feeling is that cloudpickle interaction is a bigger problem because it's widely used in the data science sphere. 🤔 |
I |
Would it help you if we made it possible to switch off cycle detection for good? I guess that would make it even fast for you? Switching off would have to be opt-in tho. |
Yeah it'd help but I'm not a huge fan of having messy decorators everywhere. That said, I don't understand the exact problem here. Cloudpickle doesn't like threadlocals in the globals of |
I get the concern here, but if there were code where repr was the hottest path and the thing most limiting performance, that would be a sign of rather deeper architectural problems. My feeling about having a special switch in the decorator for this is that it would be an excessively special case that would be useful only in a vanishingly small number of cases, and at that point the author would probably want to hand-write the Python themselves.
It’s, yeah, a bit confusing. I think the deal is that cloudpickle will try to pickle everything in a class instance itself by value, but once it goes outside that class, it will pickle by reference. Or something like that. The benefit of this is that we can limit the performance impact of this change by restructuring it: simply pull the threadlocal into another module, then reference only that module in the For comparison (and this time I didn’t screw it up): Old:
Thread-local in a different module (0d710b4):
Context-manager (2063781):
I think that cost is low enough to merit not worrying about given that it fixes real user issues. |
That does look much better. Can we add a unit test for this as well (adding cloudpickle to the dev dependencies)? |
Sorry I pressed that button by accident. |
Switched this branch over to point at the version that pulls the thread-local into |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This looks great, thanks for diving into it!
Please address the commens/suggestions and
add a news fragment: https://github.com/python-attrs/attrs/blob/main/.github/CONTRIBUTING.rst#changelog
LGTM! |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
add a news fragment
I could have sworn I had one! I must have lost it when editing history.
Squashed everything into one change so that we have one change with a nice cohesive commit message. Since apparently you can't just compare two arbitrary versions within GitHub, this reproduces the changes relative to 035280e:
|
Because cloudpickle tries to pickle a function's globals, when it pickled an attrs instance, it would try to pickle the `__repr__` method and its globals, which included a `threading.local`. This broke cloudpickle for all attrs classes unless they explicitly specified `repr=False`. Modules, however, are pickled by reference, not by value, so moving the repr into a different module means we can put `_compat` into the function's globals and not worry about direct references. Includes a test to ensure that attrs and cloudpickle remain compatible. Also adds an explanation of the reason we even *have* that global thread-local variable. It wasn't completely obvious to a reader why the thread-local was needed to track reference cycles in `__repr__` calls, and the test did not previously contain a cycle that touched a non-attrs value. This change adds a comment explaining the need and tests a cycle that contains non-attrs values. Fixes: - python-attrs#458 - cloudpipe/cloudpickle#320
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Wonderful, thank you!
Fate doesn't like to be tempted! |
Generated
__repr__
methods for Pythons with f-strings includeda
threading.local
object in that method'sglobals()
dictionary.Because
cloudpickle
attempts to serialize all globals of this method,it ends up trying to pickle the
threading.local
, which cannot bepickled.
Instead, we now pull use of the thread-local out into its own function,
which
cloudpickle
will happily serialize. As an added benefit, thiseliminates some duplicated code between f-string and non–f-string
__repr__
s.Should fix:
Pull Request Check List
I'm not sure what the best direction to go on this is. I run this locally and see that when I cloudpickle an attrs class, it works fine. It would be pretty simple to add a test dep on cloudpickle and then create and pickle an attrs class in a new test case. But would that be too much of a special-case test case for this specific library? That said, it is more likely that changes here would cause problems in cloudpickling, rather than changes in cloudpickle causing problems here, and it would be nice to catch them faster.
.pyi
)..rst
files is written using semantic newlines.changelog.d
.Another implementation idea I had would be to simply pull the
threading.local
out into another module, because that would make the global a module (which would be pickled by reference) rather than a local (which would be pickled by value). That, however, would leave essentially a two-lineimport threading; repr_context = threading.local()
module, which, ehhhhhhhh