GeezerVisonThe World Wide Web, restored to 800 pixels of glory. | ||||||||
| Home|About|Gaming cabinet|Archives|How it works|Starter sites | ||||||||
GeezerVison is showing github.com — Quadratic `compile()` / `ast.parse()` time for modules with many f-strings (PEP 701 tokenizer `update_fstring_expr` rescans the whole remaining buffer per replacement field) · Issue #155525 · python/cpython Closed Labels interpreter-core(Objects, Python, Grammar, and Parser dirs)(Objects, Python, Grammar, and Parser dirs)topic-parsertype-featureA feature request or enhancementA feature request or enhancement DescriptionBug reportBug description:SummarySince the PEP 701 f-string tokenizer landed in 3.12, parsing/compiling a module that contains many f-string replacement fields is O(n²) in the total source size, where the constant is driven by the number of It is not visible on ordinary hand-written code, but it is catastrophic for large machine-generated modules. A ~20 MB generated module in our codebase (~10k f-strings, up to ~230 plain
Environment
Minimal reproducerDependency-free. Run under 3.11 (or 3.9) vs 3.12+ and compare — the f-string compile time roughly quadruples each time the size doubles on 3.12+, but only doubles on ≤3.11. import sys, time def gen(n_fstrings, fields_each=100): # one module of `n_fstrings` f-strings, each with `fields_each` PLAIN replacement fields # (no format-spec, no conversion, no nesting). Names need not exist -- compile() only parses. field = "".join("{x%d}" % i for i in range(fields_each)) return "\n".join('s%d = f"%s"' % (j, field) for j in range(n_fstrings)) print(sys.version) prev = None for n in (500, 1000, 2000, 4000): src = gen(n) t0 = time.perf_counter() compile(src, "<gen>", "exec") dt = time.perf_counter() - t0 ratio = "" if prev is None else " (%.1fx for 2x size)" % (dt / prev) print("%5d f-strings, %5.0f KB: %7.3f s%s" % (n, len(src) / 1024, dt, ratio)) prev = dt Observed outputPython 3.9.18 — linear (~2x per size doubling): Python 3.12.4 — quadratic (~4x per size doubling): So the same 2.4 MB source is ~21x slower on 3.12 than 3.9 (1.57 s → 32.5 s), and the gap keeps widening with size. For comparison, rewriting the f-strings to Root causeThe PEP 701 tokenizer buffers each f-string replacement-field expression by copying from the current cursor to the end of the remaining source buffer on every relevant token, using 3.12.4 — static int update_fstring_expr(struct tok_state *tok, char cur) { Py_ssize_t size = strlen(tok->cur); // <-- length to END OF BUFFER, not end of expr tokenizer_mode *tok_mode = TOK_GET_MODE(tok); switch (cur) { case 0: ... strncpy(tok_mode->last_expr_buffer + tok_mode->last_expr_size, tok->cur, size); // O(remaining) ... case '{': ... tok_mode->last_expr_buffer = PyMem_Malloc(size); ... strncpy(tok_mode->last_expr_buffer, tok->cur, size); // O(remaining) ... case '}': case '!': case ':': if (tok_mode->last_expr_end == -1) { tok_mode->last_expr_end = strlen(tok->start); // O(remaining) } ... } } It is called once per A The same code (whole-remaining-buffer
Possible direction (for maintainers to evaluate)
Not a duplicate of
CPython versions tested on:3.12 Operating systems tested on:Linux Linked PRsActivity
MetadataMetadataAssigneesLabelsinterpreter-core(Objects, Python, Grammar, and Parser dirs)(Objects, Python, Grammar, and Parser dirs)topic-parsertype-featureA feature request or enhancementA feature request or enhancement ProjectsMilestoneRelationshipsDevelopmentIssue actions | ||||||||
| You are visitor 01402520 — GeezerVison does not store the pages you look up. Best viewed with Netscape Navigator 4.7 or Internet Explorer 6 at 800×600. No cookies required. |