Skip to content

Commit ec3b950

Browse files
cmaloneyclaude
andcommitted
gh-161145: Use _Py_find_first_nonascii in the tokenizer
Skip sequence of ASCII at once rather than one char per loop reusing faster code from unicodeobject. Can't use the checking from stringlib or unicodeobject fully because those assume copying/writing to an output buffer which slows down this case. Line numbers are only needed for the error message, so count them there. The function is exported because test_peg_generator builds the tokenizer sources into an extension module. The check drops from about 0.9% of compile() time to about 0.1% (perf record). CPU time per compile(), PGO+LTO, 20 paired runs on one pinned core: | Workload | main | this | runs faster | |---|---:|---:|---:| | typing.py | 6.72 ms | 1.2% faster | 17/20 | | all top-level Lib/*.py | 241 ms | 0.7% faster | 17/20 | | synthetic CJK literals | 4.32 ms | 1.3% faster | 16/20 | _PyTokenizer_ensure_utf8 instructions during compile(), callgrind: | File | main | this | |---|---:|---:| | typing.py | 1,811,154 | 132,909 | | _pydatetime.py | 1,246,691 | 91,566 | | test_email/test_contentmanager.py | 487,525 | 43,273 | | synthetic Cyrillic literals | 5,825,590 | 3,832,223 | | synthetic CJK literals | 3,450,590 | 2,479,019 | Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 parent 4a9b981 commit ec3b950

3 files changed

Lines changed: 28 additions & 16 deletions

File tree

‎Include/internal/pycore_unicodeobject.h‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -234,6 +234,13 @@ extern PyObject* _PyUnicode_FromASCII(
234234
const char *buffer,
235235
Py_ssize_t size);
236236

237+
/* Return the index of the first byte in [start, end) with the high bit set,
238+
or end - start if there is none. */
239+
// Export for test_peg_generator.
240+
PyAPI_FUNC(Py_ssize_t) _Py_find_first_nonascii(
241+
const unsigned char *start,
242+
const unsigned char *end);
243+
237244
/* Compute the maximum character of the substring unicode[start:end].
238245
Return 127 for an empty string. */
239246
extern Py_UCS4 _PyUnicode_FindMaxChar (

‎Objects/unicodeobject.c‎

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5047,8 +5047,8 @@ load_unaligned(const unsigned char *p, size_t size)
50475047
* significant bit set). If all characters in the range are ASCII, it returns
50485048
* `end - start`.
50495049
*/
5050-
static Py_ssize_t
5051-
find_first_nonascii(const unsigned char *start, const unsigned char *end)
5050+
Py_ssize_t
5051+
_Py_find_first_nonascii(const unsigned char *start, const unsigned char *end)
50525052
{
50535053
// The search is done in `size_t` chunks.
50545054
// The start and end might not be aligned at `size_t` boundaries,
@@ -5188,8 +5188,8 @@ ascii_decode(const char *start, const char *end, Py_UCS1 *dest)
51885188
return p - start;
51895189
}
51905190
#endif
5191-
Py_ssize_t pos = find_first_nonascii((const unsigned char*)start,
5192-
(const unsigned char*)end);
5191+
Py_ssize_t pos = _Py_find_first_nonascii((const unsigned char*)start,
5192+
(const unsigned char*)end);
51935193
memcpy(dest, start, pos);
51945194
return pos;
51955195
}
@@ -5348,7 +5348,8 @@ unicode_decode_utf8(const char *s, Py_ssize_t size,
53485348
const char *starts = s;
53495349
const char *end = s + size;
53505350

5351-
Py_ssize_t pos = find_first_nonascii((const unsigned char*)starts, (const unsigned char*)end);
5351+
Py_ssize_t pos = _Py_find_first_nonascii((const unsigned char*)starts,
5352+
(const unsigned char*)end);
53525353
if (pos == size) { // fast path: ASCII string.
53535354
PyObject *u = PyUnicode_New(size, 127);
53545355
if (u == NULL) {

‎Parser/tokenizer/helpers.c‎

Lines changed: 15 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
#include "pycore_runtime.h" // _Py_ID()
44
#include "pycore_token.h"
55
#include "pycore_tuple.h" // _PyTuple_FromPair
6+
#include "pycore_unicodeobject.h" // _Py_find_first_nonascii()
67

78
#include "../lexer/state.h"
89

@@ -257,14 +258,14 @@ _PyTokenizer_parser_warn(struct tok_state *tok, PyObject *category, const char *
257258
the sequence if yes, 0 if not. The special cases match
258259
those in stringlib/codecs.h:utf8_decode.
259260
*/
260-
static int
261-
valid_utf8(const unsigned char* s)
261+
static Py_ssize_t
262+
valid_utf8(const unsigned char *s, const unsigned char *end)
262263
{
263264
int expected = 0;
264265
int length;
265266
if (*s < 0x80) {
266-
/* single-byte code */
267-
return 1;
267+
/* as many ASCII chars as we can find in a row */
268+
return _Py_find_first_nonascii(s, end);
268269
}
269270
else if (*s < 0xE0) {
270271
/* \xC2\x80-\xDF\xBF -- 0080-07FF */
@@ -321,19 +322,22 @@ _PyTokenizer_ensure_utf8(const char *line, struct tok_state *tok, int lineno)
321322
{
322323
const char *badchar = NULL;
323324
const char *c;
324-
int length;
325-
const char *line_start = line;
325+
const unsigned char *end = (const unsigned char *)line + strlen(line);
326+
Py_ssize_t length;
326327
for (c = line; *c; c += length) {
327-
if (!(length = valid_utf8((const unsigned char *)c))) {
328+
if (!(length = valid_utf8((const unsigned char *)c, end))) {
328329
badchar = c;
329330
break;
330331
}
331-
if (*c == '\n') {
332-
lineno++;
333-
line_start = c + 1;
334-
}
335332
}
336333
if (badchar) {
334+
const char *line_start = line;
335+
for (c = line; c < badchar; c++) {
336+
if (*c == '\n') {
337+
lineno++;
338+
line_start = c + 1;
339+
}
340+
}
337341
_PyTokenizer_syntaxerror_at(
338342
tok, line_start, badchar - line_start + 1, lineno,
339343
-1, -1,

0 commit comments

Comments
 (0)