Skip to content

Build long strings straight into their value: one allocation each (0151) - #469

Merged
rmorozov merged 5 commits into
masterfrom
claude/project-thread-jtawod
Oct 11, 2026
Merged

rmorozov merged 5 commits into
masterfrom
claude/project-thread-jtawod

Conversation

@rmorozov

@rmorozov rmorozov commented Oct 11, 2026 •

Copy link
Copy Markdown
Member

Requested by Ruslan · project thread

Before: since 0140 P2 (#462) a string longer than 15 characters lived in a refcounted StringObject that held a std::string, so every long string a render made cost two allocations (the string's buffer, then the object), and join rebuilt its result once per item. Render allocations on string-heavy cases rose 14-32% (targets in docs/tasks/0151).

After: a long string's characters follow its StringObject in one allocation, and the code that makes strings writes them there once. All 0151 targets are met, and Render instructions drop on every string-heavy case.

join of an empty sequence is now '', as in Python (it was undefined; parity case filters.join_empty).

How. StringObject::Make(size) allocates the object plus its characters (class operator new(size_t, CharCount), matching operator deletes). Producers use InternalValue::MakeText(size, writer) (writer gets a boost::span<CharT>), InternalValue::FromText(views...), or StringBuilder (new src/string_builder.h, fmt::basic_memory_buffer with 500 characters on the stack) when the length is unknown. Converted: join, escape/forceescape, ~ on two strings, +/* on strings, slices and indexing, trim, title, upper/lower/capitalize, truncate, tojson, Value strings taken by value, string literals at load (escaped ones on the stack in template_parser.cpp). Visitors now get a heap string as a std::basic_string_view of the object (short strings are still materialised as std::basic_string); IntValue2Value copies owned text into the Value, so a user callable can keep its arguments (test CallableKeepsBuiltStringArguments). TakeString always copies now.

Two layout-only tweaks hold instruction counts at base: ApplyNewlineSequence out of line (it otherwise pulls MakeRawTextRenderer out of FineParseBlock, Load/large_static +0.8%) and ~WorkspaceMaps forced inline (GCC outlined it in Render, Render/plain_text +5 Ir).

Local counts (count.py, ce547f1 → this branch; allocations per iteration)

case target before after instructions
Render/strings 975 1,285 449 -36.8%
Render/config_file 1,120 1,281 1,071 -6.2%
Render/html_autoescape 932 1,094 748 -10.5%
Render/chat_mistral 416 491 360 -10.8%
Render/chat_llama 350 421 262 -16.6%
Render/chat_qwen 318 395 249 -16.0%
Render/filters 35 51 29 -26.9%
Load/chat_llama 59 76 59 -3.9%
Load/chat_qwen 45 57 45 -4.5%
Load/mitsuhiko_table_wide 45 51 45 -4.5%

No benchmark above base by more than +0.05% (Load/for_filter_if). Performance track verdict on CI job 114577037925: approved, no red gate cases.

Left at two allocations (none shows in the suite): indent, urlize, center, wordwrap, striptags, replace, format, ~ with a non-string operand. Listed in docs/tasks/0151.

Roles: main session; verifier 1 round (ASan+UBSan, clang C++20, ~10k differential templates vs base and Python: no behaviour change except the empty join; its blocking items were clang-tidy hits, fixed before the first push). Red pushes: 1 (461ba2d: CodeQL suspicious-add-sizeof on the characters' offset, added to a std::byte pointer; 8fe177a counts it on an unsigned char pointer).

🤖 Generated with Claude Code

https://claude.ai/code/session_01G8ZupekfuoV2DNHDYyPVmG


Generated by Claude Code

…ng (0151)

A long string value now keeps its characters in its own StringObject, right after
the object in one allocation, instead of an object holding a std::string. Visitors
see such text as a view of the object; IntValue2Value copies it into the Value.

Producers write the text straight into the value: InternalValue::MakeText and
FromText, and StringBuilder (fmt::basic_memory_buffer) for text of unknown length.
Converted: join (no longer quadratic), escape/forceescape, ~ on two strings, + and
* on strings, slices and indexing, trim, title, upper/lower/capitalize, truncate,
tojson, Value strings taken by value, and string literals at load.

join of an empty sequence is now an empty string, as in Python (was undefined).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G8ZupekfuoV2DNHDYyPVmG
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G8ZupekfuoV2DNHDYyPVmG
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G8ZupekfuoV2DNHDYyPVmG
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G8ZupekfuoV2DNHDYyPVmG
@rmorozov rmorozov self-assigned this Oct 11, 2026
Comment thread src/internal_value.h Fixed
Comment thread src/internal_value.h Fixed
CodeQL's suspicious-add-sizeof query flagged sizeof added to a std::byte
pointer; an unsigned char pointer is the unscaled byte arithmetic it expects.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G8ZupekfuoV2DNHDYyPVmG
@rmorozov
rmorozov marked this pull request as ready for review October 11, 2026 21:57
@rmorozov
rmorozov merged commit 5ce1786 into master Oct 11, 2026
39 checks passed
rmorozov pushed a commit that referenced this pull request Oct 11, 2026
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KMcWcpGwhiaEs33YjAgEV4
rmorozov added a commit that referenced this pull request Oct 11, 2026
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.

3 participants