Repository navigation
gh-159191: Add a fast path to bytes.join() for exact bytes items - #159096
christianaurichzm wants to merge 3 commits into
Conversation
… locked bytes.join() and bytearray.join() took a new reference to every exact bytes item. In the free-threaded build that is an atomic operation on objects shared between threads, and since pythonGH-158910 it runs while the list's lock is held. The critical section keeps the items alive, so borrow them, as _PyUnicode_JoinArray() does. References are taken only before something can suspend the critical section: PyObject_GetBuffer() on an item that is not bytes, or releasing the thread state to copy a large result.
|
@christianaurichzm We can do better by not using the Due to some binary layout changes the uncommon case becomes 10% slower. I was still investigating whether we can resolve that. (if not, I think the PR as is works also). (feel free to take the branch) |
When every item is an exact bytes object and the result is below the 1 MiB threshold, copy straight from the items, without filling a Py_buffer or taking a reference for each one. The caller's critical section keeps the items alive. Items could only stay borrowed under the previous commit in that same case, so the per-item borrow bookkeeping is dropped and the general path takes references as before. In the general path, release exact bytes items with a plain decref, since bytes has no bf_releasebuffer. Co-authored-by: Pieter Eendebak <pieter.eendebak@gmail.com>
|
Thanks, I pulled it in (ad4a180) and added you as co-author. With your fast path, the borrowing from my first commit doesn't buy anything anymore. Items only stayed borrowed when the whole list was exact bytes under 1 MiB, and that case now goes through the fast path. So I removed it, and the general path takes references like on main again. Other than that, I reworded the fast-path comment a bit and added a test with a one-byte separator. I can't reproduce the 10% slowdown on the uncommon cases. Release builds,
The |
For me the 10% slowdown was due to the compiler no longer inlining the |
|
@christianaurichzm Can you create a new issue for this? The PR is not really related to the issue linked to now. |
|
Done, opened gh-159191 and moved the PR there. And thanks for the |
Follow-up to #158910 (see #158910 (comment)).
When every item is exact bytes and the result is below the 1 MiB threshold for releasing the GIL, join() now copies straight from the items, without filling a
Py_bufferor taking a reference for each one. Nothing in that path runs Python code or suspends the critical section, so the list keeps the items alive, as instr.join(). Other inputs take the general path, which releases exact bytes items with a plain decref since bytes has nobf_releasebuffer. The fast path is @eendebakpt's (#159096 (comment)).Release builds,
b",".join(seq)with distinct 8-byte items, median of 7 runs:For reference, the 8-thread case was 0.35-0.55 s before #158910.
The existing
__buffer__mutation test used an immortalb'a', so it couldn't catch a freed item; it now usesbytes(2). The new FT test covers the 1 MiB path, where the critical section is suspended during the copy, andtest_joinnow covers a one-byte separator, which the fast path stores directly.-R 3:3passes on debug default and FT builds, and TSan is clean for test_free_threading, test_bytes and test_capi.test_bytes.Co-authored-by: Pieter Eendebak pieter.eendebak@gmail.com