Skip to content

Commit d07d9f4

Browse files
authored
bpo-36051: Drop GIL during large bytes.join() (pythonGH-17757)
Improve multi-threaded performance by dropping the GIL in the fast path of bytes.join. To avoid increasing overhead for small joins, it is only done if the output size exceeds a threshold.
1 parent 6a65eba commit d07d9f4

4 files changed

Lines changed: 48 additions & 19 deletions

File tree

Lib/test/test_bytes.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -547,9 +547,13 @@ def test_join(self):
547547
self.assertEqual(dot_join([bytearray(b"ab"), b"cd"]), b"ab.:cd")
548548
self.assertEqual(dot_join([b"ab", bytearray(b"cd")]), b"ab.:cd")
549549
# Stress it with many items
550-
seq = [b"abc"] * 1000
551-
expected = b"abc" + b".:abc" * 999
550+
seq = [b"abc"] * 100000
551+
expected = b"abc" + b".:abc" * 99999
552552
self.assertEqual(dot_join(seq), expected)
553+
# Stress test with empty separator
554+
seq = [b"abc"] * 100000
555+
expected = b"abc" * 100000
556+
self.assertEqual(self.type2test(b"").join(seq), expected)
553557
self.assertRaises(TypeError, self.type2test(b" ").join, None)
554558
# Error handling and cleanup when some item in the middle of the
555559
# sequence has the wrong type.

Misc/ACKS

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1106,6 +1106,7 @@ Ezio Melotti
11061106
Doug Mennella
11071107
Dimitri Merejkowsky
11081108
Brian Merrell
1109+
Bruce Merry
11091110
Alexis Métaireau
11101111
Luke Mewburn
11111112
Carl Meyer
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Drop the GIL during large ``bytes.join`` operations. Patch by Bruce Merry.

Objects/stringlib/join.h

Lines changed: 40 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,9 @@ STRINGLIB(bytes_join)(PyObject *sep, PyObject *iterable)
1818
Py_buffer *buffers = NULL;
1919
#define NB_STATIC_BUFFERS 10
2020
Py_buffer static_buffers[NB_STATIC_BUFFERS];
21+
#define GIL_THRESHOLD 1048576
22+
int drop_gil = 1;
23+
PyThreadState *save;
2124

2225
seq = PySequence_Fast(iterable, "can only join an iterable");
2326
if (seq == NULL) {
@@ -65,12 +68,21 @@ STRINGLIB(bytes_join)(PyObject *sep, PyObject *iterable)
6568
buffers[i].buf = PyBytes_AS_STRING(item);
6669
buffers[i].len = PyBytes_GET_SIZE(item);
6770
}
68-
else if (PyObject_GetBuffer(item, &buffers[i], PyBUF_SIMPLE) != 0) {
69-
PyErr_Format(PyExc_TypeError,
70-
"sequence item %zd: expected a bytes-like object, "
71-
"%.80s found",
72-
i, Py_TYPE(item)->tp_name);
73-
goto error;
71+
else {
72+
if (PyObject_GetBuffer(item, &buffers[i], PyBUF_SIMPLE) != 0) {
73+
PyErr_Format(PyExc_TypeError,
74+
"sequence item %zd: expected a bytes-like object, "
75+
"%.80s found",
76+
i, Py_TYPE(item)->tp_name);
77+
goto error;
78+
}
79+
/* If the backing objects are mutable, then dropping the GIL
80+
* opens up race conditions where another thread tries to modify
81+
* the object which we hold a buffer on it. Such code has data
82+
* races anyway, but this is a conservative approach that avoids
83+
* changing the behaviour of that data race.
84+
*/
85+
drop_gil = 0;
7486
}
7587
nbufs = i + 1; /* for error cleanup */
7688
itemlen = buffers[i].len;
@@ -102,6 +114,12 @@ STRINGLIB(bytes_join)(PyObject *sep, PyObject *iterable)
102114

103115
/* Catenate everything. */
104116
p = STRINGLIB_STR(res);
117+
if (sz < GIL_THRESHOLD) {
118+
drop_gil = 0; /* Benefits are likely outweighed by the overheads */
119+
}
120+
if (drop_gil) {
121+
save = PyEval_SaveThread();
122+
}
105123
if (!seplen) {
106124
/* fast path */
107125
for (i = 0; i < nbufs; i++) {
@@ -110,19 +128,23 @@ STRINGLIB(bytes_join)(PyObject *sep, PyObject *iterable)
110128
memcpy(p, q, n);
111129
p += n;
112130
}
113-
goto done;
114131
}
115-
for (i = 0; i < nbufs; i++) {
116-
Py_ssize_t n;
117-
char *q;
118-
if (i) {
119-
memcpy(p, sepstr, seplen);
120-
p += seplen;
132+
else {
133+
for (i = 0; i < nbufs; i++) {
134+
Py_ssize_t n;
135+
char *q;
136+
if (i) {
137+
memcpy(p, sepstr, seplen);
138+
p += seplen;
139+
}
140+
n = buffers[i].len;
141+
q = buffers[i].buf;
142+
memcpy(p, q, n);
143+
p += n;
121144
}
122-
n = buffers[i].len;
123-
q = buffers[i].buf;
124-
memcpy(p, q, n);
125-
p += n;
145+
}
146+
if (drop_gil) {
147+
PyEval_RestoreThread(save);
126148
}
127149
goto done;
128150

@@ -138,3 +160,4 @@ STRINGLIB(bytes_join)(PyObject *sep, PyObject *iterable)
138160
}
139161

140162
#undef NB_STATIC_BUFFERS
163+
#undef GIL_THRESHOLD

0 commit comments

Comments
 (0)