Skip to content

mprintf: avoid scanning strings twice for unbounded %s - #23019

Merged
cmeister2 merged 1 commit into
masterfrom
cm2/printf
Sep 21, 2026
Merged

cmeister2 merged 1 commit into
masterfrom
cm2/printf

Conversation

@cmeister2

Copy link
Copy Markdown
Contributor

Summary

Avoid scanning ordinary %s arguments twice in the shared formatter.

When precision is unspecified, out_string() already knows the exact string length, normally from strlen(). Previously, it passed that length to stream_zrun(), which scanned the same bytes again before forwarding them to stream_run().

This patch sends known-length strings directly to stream_run(). Explicit precision still uses the existing bounded, NUL-aware scan. A flow comment explains why the two paths differ. This is one shared change benefiting snprintf, maprintf, and simpleformat, not three separate optimizations.

Assembly justification

The September 21 benchmark report contains x86-64 disassembly from GCC 11.5.0 with -O2 -DNDEBUG. GCC inlined out_string() and stream_zrun() into formatf(). Both versions still call strlen() for nonempty strings with unspecified precision; the optimization removes the subsequent scan.

Before, the following consecutive instructions scan the string again. r14 points to the string, r10 holds its already-known length, and r8 is the scan index:

40df8a: test  r10,r10                  ; zero length?
40df8d: je    40e302                   ; use an empty run
40df93: movzx eax,BYTE PTR [r14]        ; reread first byte after strlen
40df97: xor   r8d,r8d                  ; scan index = 0
40df9a: jmp   40dfae
40df9c: nop   DWORD PTR [rax+0x0]       ; alignment
40dfa0: add   r8,0x1
40dfa4: cmp   r10,r8                   ; reached the already-known length?
40dfa7: jbe   40dfb2                   ; proceed to output
40dfa9: movzx eax,BYTE PTR [r14+r8*1]   ; reread next string byte
40dfae: test  al,al                    ; redundant NUL check
40dfb0: jne   40dfa0

After, a precision check bypasses that scan. In this version, r11d holds precision, r13 holds length, and r10 points to the string. The explicit-precision scan is omitted from this excerpt, as marked:

40df4c: cmp   r11d,0xffffffff          ; precision == -1?
40df50: je    40df73                   ; yes: skip the scan
; ... bounded, NUL-aware scan for explicit precision ...
40df73: mov   r8,r13                   ; pass the known length
40df76: mov   rcx,r10                  ; string
40df79: mov   rdx,r14                  ; block-output callback
40df7c: mov   rsi,rbx                  ; byte-output callback
40df7f: mov   rdi,r15                  ; sink state
40df82: mov   QWORD PTR [rsp+0x30],r9
40df87: call  40c970                   ; existing stream_run()

For each eligible string of length N, this removes N redundant byte reads, their NUL tests, and the associated scan-loop work, at the cost of a precision check. Width-only formats such as %*s also take this path: width does not impose a precision bound.

The initial length calculation and actual output copy remain. Fixed-buffer output still clamps writes to available space; allocating output retains its staging, size limits, and allocation handling. This does not introduce SIMD formatting or change the output callbacks.

Recorded performance

The report measured five alternating before/after rounds pinned to CPU 2 on an Intel Core Ultra 7 165H under WSL2, with compilation paused. Both binaries used upstream revision 1ac826a1e064b94000820adc9ac187421ce4f0db and a static GCC 11.5.0 -O2 -DNDEBUG build. Benchmark programs and inputs were unchanged.

Benchmark Before, ns/op After, ns/op Less time Redundant byte reads removed/call
snprintf 327.640 306.017 6.60% 23
maprintf 187.262 157.936 15.66% 32
simpleformat 151.608 134.567 11.24% 6

Each workload was faster in four of five paired rounds. The removed reads correspond to unspecified-precision string arguments only; they exclude strlen(), output copying, and literal-format handling. Allocation and free remain included in the allocating benchmark.

These are the report's measurements, not a new benchmark of this branch. The measured candidate also included base64 changes, and timings shifted in unchanged routines. The disassembly demonstrates elimination of the redundant scan, but the exact percentages are not isolated to this PR or guaranteed across compilers and platforms.

Safety and validation

Explicit precision is a bound, not necessarily the string length. Its scan must stop at either NUL or the precision limit, preserving both short terminated strings and non-NUL-terminated %.*s buffers. Width, padding, truncation, NULL-string handling, positional arguments, and %n accounting remain unchanged.

The build and existing formatter tests 557 and 1398 passed. Their coverage includes precision limits, non-NUL-terminated input, padding, truncation, and the va_list variants, including allocating output. checksrc and git diff --check also passed. No additional tests were needed for these already-covered behaviors; the full suite, sanitizers, and cross-platform checks were not run for this patch.

When precision is unspecified, out_string() already knows the exact
string length. Pass it directly to stream_run() instead of scanning
again in stream_zrun().

Keep the bounded, NUL-aware scan for explicit precision, including
non-NUL-terminated buffers. Document why the two paths differ.

Closes #23019
@cmeister2
cmeister2 merged commit aad438d into master Sep 21, 2026
199 of 200 checks passed
@cmeister2
cmeister2 deleted the cm2/printf branch September 21, 2026 10:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

2 participants