diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2026-11-02-05-27-54.gh-issue-161155.xXLPBV.rst b/Misc/NEWS.d/next/Core_and_Builtins/2026-11-02-05-27-54.gh-issue-161155.xXLPBV.rst new file mode 100644 index 00000000000000..ba188f96a58b30 --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2026-11-02-05-27-54.gh-issue-161155.xXLPBV.rst @@ -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. diff --git a/Objects/bytes_methods.c b/Objects/bytes_methods.c index 414afeb7bb003c..2e2236c90cdadc 100644 --- a/Objects/bytes_methods.c +++ b/Objects/bytes_methods.c @@ -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\ @@ -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]); } @@ -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]); }