Skip to content
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Speed up :meth:`str.lower`, :meth:`str.upper` and :meth:`str.casefold` for
ASCII strings, and :meth:`bytes.lower`, :meth:`bytes.upper` and their
:class:`bytearray` counterparts, by converting a machine word at a time.
Patch by Parker Loflin.
59 changes: 59 additions & 0 deletions Objects/bytes_methods.c
Original file line number Diff line number Diff line change
Expand Up @@ -242,6 +242,57 @@ _Py_bytes_istitle(const char *cptr, Py_ssize_t len)
}


/* Return 0x20 in each byte of x that lies in first..first+25, else 0,
given 0x80 - first and 0x80 - first - 26 in every byte of ge and gt.
For a byte b < 0x80, b + (0x80 - first) has its high bit set iff
b >= first, and b + (0x80 - first - 26) iff b > first + 25; neither
sum carries. "& ~x" drops bytes >= 0x80. */
static inline Py_ALWAYS_INLINE size_t
ascii_case_bits(size_t x, size_t ge, size_t gt)
{
const size_t ones = (size_t)-1 / 0xff; /* 0x01 in every byte */
size_t b = x & (0x7f * ones);
size_t ge_first = b + ge;
size_t gt_last = b + gt;
return (ge_first & ~gt_last & ~x & (0x80 * ones)) >> 2;
}

static inline Py_ALWAYS_INLINE void
ascii_case_word(char *result, const char *cptr, Py_ssize_t i,
size_t ge, size_t gt)
{
size_t x;
memcpy(&x, cptr + i, sizeof(x));
x ^= ascii_case_bits(x, ge, gt);
memcpy(result + i, &x, sizeof(x));
}

/* Requires len >= SIZEOF_SIZE_T. The last word may overlap the one before
it; converting a byte twice is harmless. Not inlined, so that short
strings skip its setup. */
static Py_NO_INLINE void
ascii_case_words(char *result, const char *cptr, Py_ssize_t len,
unsigned char first)
{
const size_t ones = (size_t)-1 / 0xff;
size_t ge = (0x80 - first) * ones, gt = (0x80 - first - 26) * ones;
Py_ssize_t i;
#if defined(_MSC_VER) && !defined(__clang__)
/* MSVC only vectorizes loops that count by one; elsewhere such a loop
gets post-indexed accesses, slow on Apple cores. Dividing unsigned
avoids an idiv in code optimized for size. */
Py_ssize_t n = (size_t)(len - 1) / SIZEOF_SIZE_T;
for (i = 0; i < n; i++) {
ascii_case_word(result, cptr, i * SIZEOF_SIZE_T, ge, gt);
}
#else
for (i = 0; i < len - SIZEOF_SIZE_T; i += SIZEOF_SIZE_T) {
ascii_case_word(result, cptr, i, ge, gt);
}
#endif
ascii_case_word(result, cptr, len - SIZEOF_SIZE_T, ge, gt);
}

PyDoc_STRVAR_shared(_Py_lower__doc__,
"B.lower() -> copy of B\n\
\n\
Expand All @@ -252,6 +303,10 @@ _Py_bytes_lower(char *result, const char *cptr, Py_ssize_t len)
{
Py_ssize_t i;

if (len >= SIZEOF_SIZE_T) {
ascii_case_words(result, cptr, len, 'A');
return;
}
for (i = 0; i < len; i++) {
result[i] = Py_TOLOWER((unsigned char) cptr[i]);
}
Expand All @@ -268,6 +323,10 @@ _Py_bytes_upper(char *result, const char *cptr, Py_ssize_t len)
{
Py_ssize_t i;

if (len >= SIZEOF_SIZE_T) {
ascii_case_words(result, cptr, len, 'a');
return;
}
for (i = 0; i < len; i++) {
result[i] = Py_TOUPPER((unsigned char) cptr[i]);
}
Expand Down
Loading