Repository navigation
gh-159191: Add a fast path to bytes.join() for exact bytes items #159096
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
base: main
Are you sure you want to change the base?
Changes from all commits
2b106e5
ad4a180
e73c74c
6794be4
ec07466
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| Speed up :meth:`bytes.join` and :meth:`bytearray.join` when all items are | ||
| :class:`bytes`. Patch by Pieter Eendebak and Christian Aurich Zanettini | ||
| Martins. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -34,6 +34,44 @@ STRINGLIB(bytes_join_lock_held)(PyObject *sep, PyObject *seq) | |
| } | ||
| } | ||
| #endif | ||
|
|
||
| Py_ssize_t limit = GIL_THRESHOLD - seplen; | ||
| for (i = 0; i < seqlen; i++) { | ||
| item = PySequence_Fast_GET_ITEM(seq, i); | ||
| if (!PyBytes_CheckExact(item) | ||
| || PyBytes_GET_SIZE(item) >= limit - sz) { | ||
| break; | ||
| } | ||
| sz += PyBytes_GET_SIZE(item) + seplen; | ||
| } | ||
| /* Fast path: all items are exact bytes and the result is too small to | ||
| * release the GIL for. Nothing here runs Python code or suspends the | ||
| * critical section, so seq keeps the items alive and they can be copied | ||
| * without taking references. */ | ||
| if (i == seqlen) { | ||
| res = STRINGLIB_NEW(NULL, sz - seplen); | ||
| if (res == NULL) { | ||
| return NULL; | ||
| } | ||
| p = STRINGLIB_STR(res); | ||
| for (i = 0; i < seqlen; i++) { | ||
| if (i != 0 && seplen != 0) { | ||
| if (seplen == 1) { | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is it really worth it to specialize this case? If you remove it, you can also remove
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I measured it, and both checks pay for themselves. Without the |
||
| *p++ = *sepstr; | ||
| } | ||
| else { | ||
| memcpy(p, sepstr, seplen); | ||
| p += seplen; | ||
| } | ||
| } | ||
| item = PySequence_Fast_GET_ITEM(seq, i); | ||
| memcpy(p, PyBytes_AS_STRING(item), PyBytes_GET_SIZE(item)); | ||
| p += PyBytes_GET_SIZE(item); | ||
|
Comment on lines
+67
to
+69
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not on this path, no. The getters themselves are fine, the problem is that the items are borrowed from a list that other threads can see. Once the GIL is released (or on the FT build, once the thread detaches and the critical section gets suspended), another thread can replace items or resize the list while we're still copying. Results over 1 MiB already go through the general path, which takes a reference and saves the buffer pointer for every item before releasing the GIL. To do it here we'd need a snapshot of the items, or a separate path just for tuples. |
||
| } | ||
| return res; | ||
| } | ||
| sz = 0; | ||
|
|
||
| if (seqlen > NB_STATIC_BUFFERS) { | ||
| buffers = PyMem_NEW(Py_buffer, seqlen); | ||
| if (buffers == NULL) { | ||
|
|
@@ -148,8 +186,17 @@ STRINGLIB(bytes_join_lock_held)(PyObject *sep, PyObject *seq) | |
| error: | ||
| res = NULL; | ||
| done: | ||
| for (i = 0; i < nbufs; i++) | ||
| PyBuffer_Release(&buffers[i]); | ||
| for (i = 0; i < nbufs; i++) { | ||
| PyObject *obj = buffers[i].obj; | ||
| if (obj != NULL && PyBytes_CheckExact(obj)) { | ||
| /* Same as PyBuffer_Release(), without the call: bytes has no | ||
| * bf_releasebuffer, so only the reference to obj is dropped. */ | ||
| Py_DECREF(obj); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't understand this change. Is it really needed?
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It is for performance. If we accept a minor performance loss for the non-exact bytes we can remove this. I would be fine with.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If it's an optimization, please add a comment explaining it.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It skips the generic |
||
| } | ||
| else { | ||
| PyBuffer_Release(&buffers[i]); | ||
| } | ||
| } | ||
| if (buffers != static_buffers) | ||
| PyMem_Free(buffers); | ||
| return res; | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.