-
-
Notifications
You must be signed in to change notification settings - Fork 33.9k
bpo-31993: do not allocate large temporary buffers in pickle dump #4353
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
bpo-31993: do not allocate large temporary buffers in pickle dump #4353
Conversation
|
Hello, and thanks for your contribution! I'm a bot set up to make sure that the project can legally accept your contribution by verifying you have signed the PSF contributor agreement (CLA). Unfortunately we couldn't find an account corresponding to your GitHub username on bugs.python.org (b.p.o) to verify you have signed the CLA (this might be simply due to a missing "GitHub Name" entry in your b.p.o account settings). This is necessary for legal reasons before we can look at your contribution. Please follow the steps outlined in the CPython devguide to rectify this issue. Thanks again to your contribution and we look forward to looking at it! |
Lib/pickle.py
Outdated
| else: | ||
| return self.file_write(data) | ||
|
|
||
| def write_chunks(self, *chunks): |
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.
Nit: I would call this write_many.
Lib/pickle.py
Outdated
| if self.current_frame and total_size >= self._FRAME_SIZE_TARGET: | ||
| # Terminate the current frame to write the next frame directly into | ||
| # the underlying file to skip the unnecessary memory allocations | ||
| # of a large temporary buffers. |
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.
Typo: "a large temporary buffers".
Lib/pickle.py
Outdated
| else: | ||
| write = self.write | ||
|
|
||
| for chunk in chunks: |
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 comment pointing out that you avoid concatenation?
2472889 to
42da6b6
Compare
Lib/pickle.py
Outdated
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.
I don't think this is needed for short bytes and strings.
Lib/pickle.py
Outdated
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.
It may be better to concatenate the opcode and the size.
|
Thanks for the quick review. I think I addressed your comments. I am not sure how to write the entry in I would appreciate it if someone else could do the C version. |
|
Yes, you should add a NEWS entry. You can (and should) use the "blurb" utility for that. I think it's fine to leave the C version for later or to someone else if you're not comfortable with that. |
|
Done. Feel free to edit it directly if you want to change anything. |
abde3f3 to
d0ee85f
Compare
|
I'm not sure about 5d46315. opcode and size_header are concatenated in any case. If there is any benefit from this change, how can it be explained? |
|
Ah, you already have reverted this change. The final variant looks pretty simple. |
|
Yes sorry I had pushed a bad intermediate version and then rebased on top of master to make sure I was benchmarking against the same master as yours. I think the intermediate commits are useless, do you want me to squash them all into a single commit? |
|
Don't squash them. It is more convenient to make a review if intermediate commits are not squashed. All them will be squashed when be merged in master. |
|
Alright. |
|
@serhiy-storchaka approved this PR but I subsequently included an implementation for the C version that has not been reviewed yet. The "awaiting-merge" label should be removed and replaced by "awaiting-review". |
Modules/_pickle.c
Outdated
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.
Why don't you simply pass the payload as a const char * instead?
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.
Oh, I see, it's because you pass it to self->write.
Modules/_pickle.c
Outdated
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.
I assumed that this is not part of the public API so it's fine to change the prototype of this function without caring about backward compat. Let me know if this is not the case.
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.
All static functions are private.
|
Do you want me to convert this script into a new non-regression test that ensures that future changes to the pickle module will not break the no-copy semantics dump for large |
serhiy-storchaka
left a comment
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.
Excellent! I didn't expect that you will go so far! And the resulting code is pretty simple.
Added few style comments and one question.
Modules/_pickle.c
Outdated
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.
Some of your comments ends with a period, but others do not. Would be better to be uniform.
Lib/pickle.py
Outdated
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.
Is it worth to create a new frame containing a single bytes/string object? This will increase the size of the file, but will not decrease the number of file reading operations (opcode + frame/bytes/string size + payload).
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.
I think the presence of a frame is required by protocol 4. The overhead is likely to be small because this code path is only there for large enough bytes instances.
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.
PEP 3154 is not very explicit whether framing is required or optional. The existing implementations (Python and C) would never dump a frameless opcode as far as I understand so it might be safer to follow that behavior in the new nocopy code path but I can change that if you think this is being too conservative.
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.
I remember that some people voiced concerns about the frame header overhead (however small it is), so I worded the PEP so that it isn't made mandatory (and the unpickler should be able to handle both with and without frames). But I do think the standard pickler implementations should always emit frames in protocol 4.
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.
Frames are optional.
If don't create a frame for a separate bytes object, this could decrease memory usage on unpickling. You could directly use the result of the read() method without creating a buffer.
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.
Indeed I observed at least one useless memory copy at load time for large objects. I have not investigated yet, however, it's there both with the frameless protocol 3 and with protocol 4 with framing enabled. Anyway, it would be best to choose a design that allows for no-copy semantics of large objects both at dump and load times.
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.
There are no other picklers except the C-based and Python-based picklers. 😸
Modules/_pickle.c
Outdated
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.
All static functions are private.
Modules/_pickle.c
Outdated
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.
New PEP 7 rule requires braces even around the if body containing a simple return statement. The code in this module is older than this rule, but the new code should obey it.
Modules/_pickle.c
Outdated
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.
The same doubt as about the Python code. Is creating a new frame needed here?
|
It would be more interesting to see a benchmark with larger objects (for example just a bit smaller than 64kiB?). |
|
I ran a benchmark with 63 MB of 63kB random bytes objects and there is no performance regression when using getvalue in this PR vs master:
with |
|
@pitrou do you want me to push the variant with the call to |
|
No, let's keep
Imposing a copy on everyone is pointless. |
|
I am in favor of:
that is: what is currently implemented in this PR. This way the Python-based pickler behaves closer to the C implementation that allows delayed access to the bytes of the frame contents without having to force a copy. |
pitrou
left a comment
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.
I think this is mostly good to go now. Just two small comments.
Modules/_pickle.c
Outdated
| static int | ||
| _Pickler_write_large_bytes( | ||
| PicklerObject *self, const char *header, Py_ssize_t header_size, | ||
| PyObject *payload, Py_ssize_t payload_size) |
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.
payload_size doesn't seem used below.
| if(_Pickler_CommitFrame(self)) { | ||
| return -1; | ||
| } | ||
| if (self->write != NULL) { |
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.
Would be nice to add a comment here.
|
A Python core developer has requested some changes be made to your pull request before we can consider merging it. If you could please address their requests along with any other requests in other reviews from core developers that would be appreciated. Once you have made the requested changes, please leave a comment on this pull request containing the phrase |
|
@pitrou @serhiy-storchaka I have made the requested changes; please review again. |
|
Thanks for making the requested changes! @serhiy-storchaka, @pitrou: please review the changes made to this pull request. |
|
Thanks for your work @ogrisel ! Unless @serhiy-storchaka chimes in soon I'm gonna merge this pull request. |
|
Let me to look on this again. |
serhiy-storchaka
left a comment
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.
LGTM! I have found just few typos.
I don't have opinion about getbuffer/getvalue. Let defer it until we encounter concrete issue.
Lib/pickle.py
Outdated
| f.truncate() | ||
| data = f.getbuffer() | ||
| write = self.file_write | ||
| # Issue a single call to the write nethod of the underlying |
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.
s/nethod/method/
Modules/_pickle.c
Outdated
| * to limit memory usage when dumping large complex objects to | ||
| * a file. | ||
| * | ||
| * self-write is NULL when called via dumps. |
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.
self->write
|
Thanks for the review @serhiy-storchaka, I fixed the remaining typos. |
For all protocols: avoid concatenating large bytes and str with their opcode
and size header but instead issue an individual call to self.write(data).
For protocol 4: if the size of the opcode + size header + data is larger than
the target frame size, commit the current frame and bypass io.BytesIO to write
the next frame directly to the underlying file object.
https://bugs.python.org/issue31993
Edit: this now includes changes both the Python and C implementations of the picklers.