Skip to content

Commit b4d59af

Browse files
committed
gh-161155: Speed up ASCII lower() and upper() for str, bytes and bytearray
_Py_bytes_lower() and _Py_bytes_upper() now convert strings of at least one machine word (8 bytes on 64-bit platforms, 4 on 32-bit) a word at a time with integer arithmetic. When the length is not a multiple of the word size, the last word overlaps the one before it. Shorter strings still take the existing table loop. str.lower(), str.upper() and str.casefold() use these functions for ASCII strings, as do lower(), upper() and capitalize() on bytes and bytearray.
1 parent 4e0677a commit b4d59af

2 files changed

Lines changed: 58 additions & 0 deletions

File tree

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
Speed up :meth:`str.lower`, :meth:`str.upper` and :meth:`str.casefold` for
2+
ASCII strings, and :meth:`bytes.lower`, :meth:`bytes.upper` and their
3+
:class:`bytearray` counterparts, by converting a machine word at a time.
4+
Patch by Parker Loflin.

‎Objects/bytes_methods.c‎

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -242,6 +242,52 @@ _Py_bytes_istitle(const char *cptr, Py_ssize_t len)
242242
}
243243

244244

245+
/* Return 0x20 in each byte of the word x that is in the range
246+
first..first+25 ('A'..'Z' or 'a'..'z'), and 0 in every other byte.
247+
XOR-ing the result into x switches the case of exactly those letters.
248+
249+
For a byte b below 0x80, b + (0x80 - first) has its high bit set iff
250+
b >= first, and b + (0x80 - first - 26) has its high bit set iff
251+
b > first + 25. The sums stay below 0x100, so nothing carries into the
252+
next byte. Bytes 0x80 and above are dropped by "& ~x". Every step works
253+
on each byte separately, so the result depends neither on byte order nor
254+
on the size of size_t. */
255+
static inline Py_ALWAYS_INLINE size_t
256+
ascii_case_bits(size_t x, unsigned char first)
257+
{
258+
const size_t ones = (size_t)-1 / 0xff; /* 0x01 in every byte */
259+
size_t b = x & (0x7f * ones);
260+
size_t ge_first = b + (0x80 - first) * ones;
261+
size_t gt_last = b + (0x80 - first - 26) * ones;
262+
return (ge_first & ~gt_last & ~x & (0x80 * ones)) >> 2;
263+
}
264+
265+
/* Convert the word at cptr + i into result + i. */
266+
static inline Py_ALWAYS_INLINE void
267+
ascii_case_word(char *result, const char *cptr, Py_ssize_t i,
268+
unsigned char first)
269+
{
270+
size_t x;
271+
memcpy(&x, cptr + i, sizeof(x));
272+
x ^= ascii_case_bits(x, first);
273+
memcpy(result + i, &x, sizeof(x));
274+
}
275+
276+
/* Convert len >= SIZEOF_SIZE_T bytes a word at a time. If len is not a
277+
multiple of the word size, the last word overlaps the one before it;
278+
converting a byte twice gives the same result as converting it once.
279+
Kept out of line so that short strings don't pay for its setup. */
280+
static Py_NO_INLINE void
281+
ascii_case_words(char *result, const char *cptr, Py_ssize_t len,
282+
unsigned char first)
283+
{
284+
Py_ssize_t i;
285+
for (i = 0; i < len - SIZEOF_SIZE_T; i += SIZEOF_SIZE_T) {
286+
ascii_case_word(result, cptr, i, first);
287+
}
288+
ascii_case_word(result, cptr, len - SIZEOF_SIZE_T, first);
289+
}
290+
245291
PyDoc_STRVAR_shared(_Py_lower__doc__,
246292
"B.lower() -> copy of B\n\
247293
\n\
@@ -252,6 +298,10 @@ _Py_bytes_lower(char *result, const char *cptr, Py_ssize_t len)
252298
{
253299
Py_ssize_t i;
254300

301+
if (len >= SIZEOF_SIZE_T) {
302+
ascii_case_words(result, cptr, len, 'A');
303+
return;
304+
}
255305
for (i = 0; i < len; i++) {
256306
result[i] = Py_TOLOWER((unsigned char) cptr[i]);
257307
}
@@ -268,6 +318,10 @@ _Py_bytes_upper(char *result, const char *cptr, Py_ssize_t len)
268318
{
269319
Py_ssize_t i;
270320

321+
if (len >= SIZEOF_SIZE_T) {
322+
ascii_case_words(result, cptr, len, 'a');
323+
return;
324+
}
271325
for (i = 0; i < len; i++) {
272326
result[i] = Py_TOUPPER((unsigned char) cptr[i]);
273327
}

0 commit comments

Comments
 (0)