Skip to content

Conversation

@ogrisel
Copy link
Contributor

@ogrisel ogrisel commented Nov 9, 2017

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.

@the-knights-who-say-ni
Copy link

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):
Copy link
Member

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.
Copy link
Member

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:
Copy link
Member

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?

@ogrisel ogrisel force-pushed the issue-31993-pypickle-dump-mem-optim branch from 2472889 to 42da6b6 Compare November 9, 2017 18:25
Lib/pickle.py Outdated
Copy link
Member

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
Copy link
Member

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.

@ogrisel
Copy link
Contributor Author

ogrisel commented Nov 9, 2017

Thanks for the quick review.

I think I addressed your comments. I am not sure how to write the entry in NEWS.d. I think this PR is useful as it is and we should not wait for the C version. Shall I add an entry for the Python _Pickler alone in the NEWS file for 3.7.0a2?

I would appreciate it if someone else could do the C version.

@pitrou
Copy link
Member

pitrou commented Nov 9, 2017

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.

@ogrisel
Copy link
Contributor Author

ogrisel commented Nov 9, 2017

Done. Feel free to edit it directly if you want to change anything.

@ogrisel ogrisel force-pushed the issue-31993-pypickle-dump-mem-optim branch 2 times, most recently from abde3f3 to d0ee85f Compare November 10, 2017 14:36
@serhiy-storchaka
Copy link
Member

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?

@serhiy-storchaka
Copy link
Member

Ah, you already have reverted this change. The final variant looks pretty simple.

@ogrisel
Copy link
Contributor Author

ogrisel commented Nov 10, 2017

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?

@serhiy-storchaka
Copy link
Member

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.

@ogrisel
Copy link
Contributor Author

ogrisel commented Nov 10, 2017

Alright.

@ogrisel
Copy link
Contributor Author

ogrisel commented Nov 11, 2017

@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".

Copy link
Member

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?

Copy link
Member

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.

@serhiy-storchaka serhiy-storchaka self-requested a review November 11, 2017 20:39
@serhiy-storchaka serhiy-storchaka added the type-feature A feature request or enhancement label Nov 11, 2017
Copy link
Contributor Author

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.

Copy link
Member

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.

@ogrisel
Copy link
Contributor Author

ogrisel commented Nov 12, 2017

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 bytes & utf-8 backed str?

Copy link
Member

@serhiy-storchaka serhiy-storchaka left a 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.

Copy link
Member

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
Copy link
Member

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).

Copy link
Contributor Author

@ogrisel ogrisel Nov 12, 2017

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.

Copy link
Contributor Author

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.

Copy link
Member

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.

Copy link
Member

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.

Copy link
Contributor Author

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.

Copy link
Member

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. 😸

Copy link
Member

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.

Copy link
Member

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.

Copy link
Member

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?

@pitrou
Copy link
Member

pitrou commented Dec 13, 2017

It would be more interesting to see a benchmark with larger objects (for example just a bit smaller than 64kiB?).

@ogrisel
Copy link
Contributor Author

ogrisel commented Dec 13, 2017

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:

  • master:
# protocol 4, output: output.pkl
median time: 0.23s
# protocol 4, output: /dev/null
median time: 0.0262s
last pickle size: 63.009MB
  • this PR with getvalue instead of getbuffer
# protocol 4, output: output.pkl
median time: 0.23s
# protocol 4, output: /dev/null
median time: 0.0256s
last pickle size: 63.009MB

with getbuffer it's the same speed as master. So maybe getvalue is slightly faster but the difference is probably not significant.

@ogrisel
Copy link
Contributor Author

ogrisel commented Dec 14, 2017

@pitrou do you want me to push the variant with the call to getvalue()?

@pitrou
Copy link
Member

pitrou commented Dec 14, 2017

No, let's keep getbuffer. I don't think there's any reason to use getvalue. The two alternatives are:

  • use getbuffer and release the buffer immediately (so that a writer wanting to keep the buffer in memory has to make a copy first)
  • use getbuffer and not release it

Imposing a copy on everyone is pointless.

@ogrisel
Copy link
Contributor Author

ogrisel commented Dec 18, 2017

I am in favor of:

  • use getbuffer and not release it

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.

Copy link
Member

@pitrou pitrou left a 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.

static int
_Pickler_write_large_bytes(
PicklerObject *self, const char *header, Py_ssize_t header_size,
PyObject *payload, Py_ssize_t payload_size)
Copy link
Member

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) {
Copy link
Member

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.

@bedevere-bot
Copy link

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 I have made the requested changes; please review again. I will then notify any core developers who have left a review that you're ready for them to take another look at this pull request.

@ogrisel
Copy link
Contributor Author

ogrisel commented Jan 4, 2018

@pitrou @serhiy-storchaka I have made the requested changes; please review again.

@bedevere-bot
Copy link

Thanks for making the requested changes!

@serhiy-storchaka, @pitrou: please review the changes made to this pull request.

@pitrou
Copy link
Member

pitrou commented Jan 4, 2018

Thanks for your work @ogrisel ! Unless @serhiy-storchaka chimes in soon I'm gonna merge this pull request.

@serhiy-storchaka
Copy link
Member

Let me to look on this again.

Copy link
Member

@serhiy-storchaka serhiy-storchaka left a 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
Copy link
Member

Choose a reason for hiding this comment

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

s/nethod/method/

* to limit memory usage when dumping large complex objects to
* a file.
*
* self-write is NULL when called via dumps.
Copy link
Member

Choose a reason for hiding this comment

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

self->write

@ogrisel
Copy link
Contributor Author

ogrisel commented Jan 5, 2018

Thanks for the review @serhiy-storchaka, I fixed the remaining typos.

@serhiy-storchaka serhiy-storchaka merged commit 3cd7c6e into python:master Jan 6, 2018
@ogrisel ogrisel deleted the issue-31993-pypickle-dump-mem-optim branch January 6, 2018 16:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

type-feature A feature request or enhancement

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants