Uh oh!
There was an error while loading. Please reload this page.
perf: stop flushing per token while writing the final parse tree (1.10x) - #720
Merged
Conversation
Profiling a compiled parse-en-us run after 3.8.7 left ~13% of self time in ntdll!ZwWriteFile. That is not write volume: a plain ofstream writes the same 6.1 MB in 0.034s at 170 MB/s. The stacks pointed at Parse::finExecute -> Parse::finalTree -> Tree<Pn>::Traverse -> operator<< -> Iarg::genArg -> std::flush Parse::finalTree writes a 3 MB final.tree, and Iarg::genArg/genArgs flushed after nearly every fragment they emitted -- 22 std::flush calls on that path, so each node's attribute list cost a handful of WriteFile syscalls. Same defect as the Arun::out per-write flush fixed in 3.8.5, in a different writer. Removed; the stream still flushes when it closes, so the file on disk is unchanged. Interleaved A/B, 8 runs per arm, swapping only nlp.exe: flush per token min 7.69s p25 7.73s median 8.12s buffered min 7.00s p25 7.00s median 7.16s 1.10x All 18 analyzer output files byte-identical, final.tree included (3,085,335 bytes before and after). Version note: assumes #718 (3.8.8) lands first. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
ddehilsterforce-pushed
the
perf/tree-dump-flush
branch
from
August 25, 2026 17:11
27f1689 to
65705c8Compareddehilster
commented
Aug 25, 2026
MemberAuthor
Rebased onto master now that #718 and #721 have landed, and resolved the version to 3.8.9 (master is at 3.8.8). Only the version line needed resolving — this PR touches This run is also the first real test of the vcpkg binary cache from #721: the entries were saved on that PR's run, so |
Uh oh!
There was an error while loading. Please reload this page.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for freeto join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What I was chasing
After 3.8.7, a profile of a compiled parse-en-us run still had ~13% of self time in
ntdll!ZwWriteFile. My first guess was the write path itself —lite/var.cppopens every NLP++ output file withstd::ios::app, which on the MSVC CRT repositions to end-of-file before every write.That guess was wrong, and worth recording. A standalone benchmark writing the same 6.1 MB through
std::ofstreamin 220k small<<calls:Append mode costs 13%, buffer size 5%, and the whole 6 MB costs 34 ms. So neither append mode nor buffering explains ~1.3s — buffered stream writes were never the problem.
What it actually is
Going back to the stacks:
Parse::finalTreewrites a 3 MBfinal.tree, and Iarg::genArg / genArgs flushed after nearly every fragment they emitted — 22std::flushcalls on that path, so each node's attribute list costs a handful ofWriteFilesyscalls. It's the same defect as theArun::outper-write flush fixed in 3.8.5, just in a different writer.Result
Removed. The stream still flushes when it closes, so the file on disk is unchanged.
Interleaved A/B, 8 runs per arm, swapping only
nlp.exe:1.10x.
Correctness
All 18 analyzer output files byte-identical,
final.treeincluded (3,085,335 bytes before and after).Iarg::genArgis also used when writing generated.nlprule files. That's a bulk writer too and it flushes on close, so the content is unaffected there as well — but flagging it since it's a second caller.Version
Assumes #718 (3.8.8) lands first. If it doesn't, this should be 3.8.8.
🤖 Generated with Claude Code