Skip to content

perf: template root and raw text own nothing; lighter parse scaffolding (0118 P5b-2) - #437

Merged
rmorozov merged 2 commits into
masterfrom
claude/perf-wave2-arena-slots-p6gl1u
Oct 8, 2026
Merged

rmorozov merged 2 commits into
masterfrom
claude/perf-wave2-arena-slots-p6gl1u

Conversation

@rmorozov

@rmorozov rmorozov commented Oct 8, 2026

Copy link
Copy Markdown
Member

🤖 Generated with Claude Code

https://claude.ai/code/session_01QT4pkibQA9uD34K5vYkC2j

Before: a template's root node keeps its blocks in an unordered_map. A raw text node holds a shared_ptr to its text whenever newline_sequence changes the text. Every Load also allocates a std::list node for the statement stack and zero-fills the arena's 512-byte inline block.

After: the root and raw text nodes own nothing, so the arena neither destroys them nor relocates them through a hook. The statement stack and the arena scaffolding cost no allocation or fill. Rendering is unchanged. Load plain_text drops from 4,320 to 4,038 instructions, below the wave 2 target of 4,121.

How (two commits):

  • P5b-2a (template root, raw text).
    • TemplateRenderer keeps its blocks as an ArenaSpan<NodeRef<BlockStatement>>. The parser collects them in TemplateRootInfo and sets the span when the parse ends.
    • A duplicate block name is still a parse error. The check compares names one by one up to 16 blocks and uses a std::unordered_set<std::string_view> beyond that. The verifier found the linear-only version quadratic on hostile templates: 100k blocks took 51 s.
    • Text converted to newline_sequence lives in a std::forward_list that the template owns next to its source.
    • Both kinds join the static_assert list of trivially destructible nodes.
  • P5b-2b (parse scaffolding).
    • StatementInfoList is a boost::container::small_vector<StatementInfo, 4> instead of std::list. The verifier audited all users: none holds a reference across a push.
    • NodeArena's inline block and OffsetList's inline array are no longer zero-filled. Seal reads only the bytes and offsets that were made; valgrind memcheck is clean.
  • Tests:
    • BasicTests.NewlineSequenceTextOutlivesParse (narrow and wide, inside a block, rendered twice).
    • BasicTests.ManyBlocksRejectADuplicateName (40 blocks, duplicates at positions 3 and 39).
  • Not in this PR: the plan's constants and keep-alive side tables. They were built and measured, then dropped: Load got worse by +0.3% to +7% and Render by up to +1.7%. The perf track accepted the deviation, and the negative result is recorded in docs/tasks/0118.

Numbers (bench/count.py, Release, NODEREF_CHECKS=ON)

Load against the 0892811 targets:

case target before (#436) after vs #436
plain_text 4,121 4,456 4,038 (-2.0% vs target) -9.4%
substitute 11,103 11,287 10,932 -3.1%
for_range 20,469 21,692 21,339 -1.6%
dict_ops 51,492 53,980 53,574 -0.8%
for_filter_if 42,262 43,950 42,639 -3.0%
inheritance 37,608 38,324 36,456 -4.9%
for_loop_vars 59,590 59,328 58,810 -0.9%
  • The largest Load rises are against P5b-2a, from the 2b commit: large_static +0.46%, chat_llama +0.40%. Both are inside the +0.5% per-PR gate.
  • Render: every case is within ±0.35%.
  • Retained bytes (cap in brackets): plain_text 688 (848), substitute 880 (1,088), for_range 1,352 (1,648), inheritance 1,632 (2,568).

Process

  • Roles: the main session implemented and measured; there were two verifier rounds. The architect's P5b plan (from perf: destroy only the arena nodes that own something (0118 P5b-1) #436) covered the dropped tables.
  • Verifier round 1 (2a): one finding, the quadratic duplicate-block check. It is fixed, and a test was added. The round found no lifetime issue with converted texts on reload, failed reload, include, import or extends. All 42 block-semantics cases matched Python Jinja2. A differential run of the 2,424-file fuzz corpus with \r\n and \r showed 0 differences, and about 400k ASan fuzz iterations were clean.
  • Verifier round 2 (2a fix + 2b): no findings. It ran ASan+UBSan with clang; a NODEREF=OFF Release build; fuzz replay; valgrind memcheck with origin tracking (0 arena errors); 1,746 deeply nested templates against Python Jinja2 with 0 mismatches; and the many-blocks timing, which is linear.
  • Red pushes: none so far.

Generated by Claude Code

claude added 2 commits October 8, 2026 05:13
…template (0118 P5b-2a)

TemplateRenderer's block map becomes an ArenaSpan of BlockStatement refs, filled
at the end of the parse from a parser-side TemplateRootInfo, and RawTextRenderer
drops its shared_ptr holder: text converted to newline_sequence lives in a
template-owned list. Both nodes now own nothing, so Seal neither relocates nor
destroys them through hooks.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QT4pkibQA9uD34K5vYkC2j
The stack of open statements is a small_vector of four instead of a
std::list, so the root costs no allocation, and the arena's inline block
and offset list are left uninitialized instead of zero-filled: Seal reads
only the bytes and offsets made. Load plain_text -6.5%, substitute -2.1%.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QT4pkibQA9uD34K5vYkC2j
@rmorozov rmorozov self-assigned this Oct 8, 2026
@rmorozov
rmorozov marked this pull request as ready for review October 8, 2026 06:32
@rmorozov
rmorozov merged commit f3cc856 into master Oct 8, 2026
39 checks passed
rmorozov pushed a commit that referenced this pull request Oct 8, 2026
…e cache)

Filed from the 0118 P5b and 0117 P2 reviews. Also adds the missing index
rows for 0145 and 0146 and closes 0146 (option 2 merged in #435; the perf
track no longer needs option 1 since #437 brought plain_text under target).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01M4RSuk9cfqu9jdeaWMRDKW
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