Skip to content

Commit da479ac

Browse files
committed
gh-153568: Cache repeated identifiers while parsing
Identifiers repeat constantly in real code, so the parser now reuses the interned name created the first time it sees a given spelling.
1 parent 106eb53 commit da479ac

3 files changed

Lines changed: 50 additions & 0 deletions

File tree

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Speed up the parser by caching repeated identifiers during a parse.

Parser/pegen.c

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -572,11 +572,41 @@ _PyPegen_name_from_token(Parser *p, Token* t)
572572
p->error_indicator = 1;
573573
return NULL;
574574
}
575+
// Identifiers repeat constantly; a small span-keyed cache skips the
576+
// UTF-8 decode + intern for repeated occurrences. Keys point into
577+
// arena-owned token bytes and values are arena-owned interned strings,
578+
// so borrowed references are valid for the lifetime of the parse
579+
// (including the second error pass, which reuses parser and arena).
580+
Py_ssize_t len = PyBytes_GET_SIZE(t->bytes);
581+
uint32_t hash = 2166136261u;
582+
for (Py_ssize_t i = 0; i < len; i++) {
583+
hash = (hash ^ (unsigned char)s[i]) * 16777619u;
584+
}
585+
struct _identifier_cache_entry *free_slot = NULL;
586+
size_t idx = hash & (_PYPEGEN_IDENT_CACHE_SIZE - 1);
587+
for (int probe = 0; probe < _PYPEGEN_IDENT_CACHE_MAX_PROBES; probe++) {
588+
struct _identifier_cache_entry *e =
589+
&p->ident_cache[(idx + probe) & (_PYPEGEN_IDENT_CACHE_SIZE - 1)];
590+
if (e->key == NULL) {
591+
free_slot = e;
592+
break;
593+
}
594+
if (e->hash == hash && e->len == len && memcmp(e->key, s, len) == 0) {
595+
return _PyAST_Name(e->value, Load, t->lineno, t->col_offset,
596+
t->end_lineno, t->end_col_offset, p->arena);
597+
}
598+
}
575599
PyObject *id = _PyPegen_new_identifier(p, s);
576600
if (id == NULL) {
577601
p->error_indicator = 1;
578602
return NULL;
579603
}
604+
if (free_slot != NULL) {
605+
free_slot->key = s;
606+
free_slot->len = len;
607+
free_slot->hash = hash;
608+
free_slot->value = id;
609+
}
580610
return _PyAST_Name(id, Load, t->lineno, t->col_offset, t->end_lineno,
581611
t->end_col_offset, p->arena);
582612
}
@@ -844,6 +874,15 @@ _PyPegen_Parser_New(struct tok_state *tok, int start_rule, int flags,
844874
p->flags = flags;
845875
p->feature_version = feature_version;
846876
p->known_err_token = NULL;
877+
p->ident_cache = PyMem_Calloc(_PYPEGEN_IDENT_CACHE_SIZE,
878+
sizeof(struct _identifier_cache_entry));
879+
if (p->ident_cache == NULL) {
880+
growable_comment_array_deallocate(&p->type_ignore_comments);
881+
PyMem_Free(p->tokens[0]);
882+
PyMem_Free(p->tokens);
883+
PyMem_Free(p);
884+
return (Parser *) PyErr_NoMemory();
885+
}
847886
p->level = 0;
848887
p->call_invalid_rules = 0;
849888
p->last_stmt_location.lineno = 0;
@@ -859,6 +898,7 @@ _PyPegen_Parser_New(struct tok_state *tok, int start_rule, int flags,
859898
void
860899
_PyPegen_Parser_Free(Parser *p)
861900
{
901+
PyMem_Free(p->ident_cache);
862902
Py_XDECREF(p->normalize);
863903
for (int i = 0; i < p->size; i++) {
864904
PyMem_Free(p->tokens[i]);

Parser/pegen.h

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,8 +91,17 @@ typedef struct {
9191
int call_invalid_rules;
9292
int debug;
9393
location last_stmt_location;
94+
struct _identifier_cache_entry {
95+
const char *key; // borrowed; points into arena-owned token bytes
96+
Py_ssize_t len;
97+
uint32_t hash;
98+
PyObject *value; // borrowed; arena-owned interned identifier
99+
} *ident_cache;
94100
} Parser;
95101

102+
#define _PYPEGEN_IDENT_CACHE_SIZE 2048 // power of two
103+
#define _PYPEGEN_IDENT_CACHE_MAX_PROBES 8
104+
96105
typedef struct {
97106
cmpop_ty cmpop;
98107
expr_ty expr;

0 commit comments

Comments
 (0)