Skip to content

Load: halve the cost of loading tag-heavy templates (0103) - #377

Merged
rmorozov merged 9 commits into
masterfrom
claude/project-thread-wu2i55
Oct 4, 2026
Merged

rmorozov merged 9 commits into
masterfrom
claude/project-thread-wu2i55

Conversation

@rmorozov

@rmorozov rmorozov commented Oct 4, 2026 •

Copy link
Copy Markdown
Member

Requested by Ruslan · project thread

Before: loading Load/many_tags took 44.15M instructions and 34,249 allocations. Most of that was the parser looking up keywords again at every precedence level for every token, copying tokens and parse results level by level, and copying Settings for every statement tag.

After: the same load takes 21.99M instructions (2.01x cheaper) and 16,559 allocations (-52%). Every other Load/ case is 31-51% cheaper, and Render/ is unchanged. Rendered output and error messages are byte-identical to master, with one exception that now matches Python (below).

How:

  • The lexer classifies each symbol once and stores the result in Token::keyword; the parsers read it instead of calling GetKeyword. FindKeyword indexes by first character.
  • The lexertk generator and token vector are reused across tags (LexBuffers), and tokens are built in place.
  • Each precedence level keeps one result variable so NRVO applies, ParseUnaryPlusMinus calls the postfix and filter parsers only when the next token can start them, and token copies became references.
  • StatementsParser holds const Settings& instead of a copy; statement infos are moved, not copied.
  • Smaller: MarkMacroSpecialNames returns at once outside macros, attribute subscripts no longer allocate a constant node, IsMethodName binary-searches, FindBlockEnd skips plain characters, MatchTagAt gates raw/meta on the first letter.

Deliberate behaviour change (parity fix): a quoted keyword such as 'in' or 'if' used to be treated as the keyword, because GetKeyword read the token's source text. It is now a string, as in Python Jinja2, so {{ 1 'in' l }} is a syntax error and {% for a in l, 'if' %} renders. New cases in test/parity/cases/errors.py pin it.

bench/count.py --baseline (master before → this branch)

Benchmark Instructions Baseline Change Allocations Bytes
Load/dict_ops 80,324 148,691 -45.98% 88 (-43) 11,001 (-5,576)
Load/expressions 105,233 206,111 -48.94% 103 (-43) 12,151 (-10,344)
Load/filters 163,195 274,433 -40.53% 153 (-75) 18,670 (-14,248)
Load/for_filter_if 69,984 135,397 -48.31% 80 (-43) 9,100 (-4,808)
Load/for_loop_vars 88,595 180,600 -50.94% 92 (-65) 10,965 (-8,472)
Load/for_range 33,542 56,665 -40.81% 47 (-9) 5,183 (-552)
Load/inheritance 59,016 113,566 -48.03% 68 (-39) 7,586 (-3,912)
Load/large_static 608,896 886,894 -31.35% 244 (-196) 76,399 (-15,576)
Load/macros 122,848 229,302 -46.43% 127 (-59) 18,221 (-11,208)
Load/many_tags 21,994,335 44,153,332 -50.19% 16,559 (-17,690) 1,801,775 (-2,953,352)
Load/mitsuhiko_table 124,182 237,726 -47.76% 133 (-51) 19,504 (-5,736)
Load/plain_text 9,584 9,676 -0.95% 18 1,610
Load/strings 156,142 294,038 -46.90% 142 (-82) 16,560 (-20,808)
Load/substitute 19,669 29,857 -34.12% 33 (-4) 2,963 (-216)

Render/ cases are all within -3.6%..+0.0% (no render code changed; for_loop_vars -3.6% comes from cheaper attribute subscripts).

Tasks

Process

  • Roles: main session explored and implemented; verifier 1 round (ASan+UBSan, fuzz replay over 2,544 inputs, C++20 build, clang-format, output/error diff vs base). It found the quoted-keyword change, now pinned by corpus cases. Notes it raised: IsEqual on attribute subscripts compares null expr pointers (no practical effect); possible MSVC C4996 on 4-argument std::equal; clang-tidy 22 not run locally (CI checks it).
  • master merged in to resolve the docs/tasks/README.md conflict from the renumbering, and again (a774747) before reporting final to the merge steward; tests pass on both merges.
  • Red pushes in CI: 1. MSVC C++17 Release failed with C3493: a non-static local constexpr (charsCount in FindKeyword) used inside a capture-less lambda, which GCC, Clang and MSVC C++20 accept. Fixed by making it static constexpr (f770c6f).

🤖 Generated with Claude Code

https://claude.ai/code/session_01SGHFyqdC8NgXXsL6PatsQP

claude added 3 commits October 3, 2026 23:21
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SGHFyqdC8NgXXsL6PatsQP
…per rough split

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SGHFyqdC8NgXXsL6PatsQP
Quoted keywords ('in', 'if', 'not', ...) are strings, never keywords, as in
Python Jinja2; the errors corpus now covers that.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SGHFyqdC8NgXXsL6PatsQP
@rmorozov rmorozov self-assigned this Oct 4, 2026
claude added 6 commits October 4, 2026 00:01
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SGHFyqdC8NgXXsL6PatsQP
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SGHFyqdC8NgXXsL6PatsQP
…ad-wu2i55

# Conflicts:
#	docs/tasks/README.md
MSVC (C++17, C3493) refuses to use a non-static local constexpr inside a
capture-less lambda without capturing it.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SGHFyqdC8NgXXsL6PatsQP
@rmorozov
rmorozov marked this pull request as ready for review October 4, 2026 03:28
@rmorozov
rmorozov merged commit 65f7a31 into master Oct 4, 2026
40 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants