Skip to content

<print>: Optimize no-argument print() brace handling - #6465

Open
Cornea Cristian (cristi1990an) wants to merge 9 commits into
microsoft:mainfrom
cristi1990an:print-noformat-escape-brace-optimization
Open

Cornea Cristian (cristi1990an) wants to merge 9 commits into
microsoft:mainfrom
cristi1990an:print-noformat-escape-brace-optimization

Conversation

@cristi1990an

@cristi1990an Cornea Cristian (cristi1990an) commented Sep 25, 2026 •

Copy link
Copy Markdown

Optimization for std::print calls with no format arguments, no new lines and no escaped braces. Current code was allocating an intermediary string by calling _Unescape_braces even if the format string contained no such braces. This might seem like a very niche and constrained case but in my opinion it represents the most common real world case of a print call without format arguments. Allocating an unnecessary string for std::print("Hello long string world ... ") is surprising imo.

Optimization is simple in theory, a bit trickier to write down in practice... We do an iteration looking for escaped braces in the format string and if we don't find any, we just call _Vprint... with the string_view. An extended optimization is made here such that if we do find an escape brace at the end of a long string, we don't waste the compute of the search, _Find_escape_braces returns the offset of the found "{{" or "}}" and _Unescape_braces signature is extended with _Escape_brace_offset parameter. The substring _Old_str[0, _Escape_brace_offset) is then copied directly to the resulting string since we've guaranteed no "{{" or "}}" were found in this prefix range.

Edit: after gaining a better understand of the msvc stl\stl\src\print.cpp design I realize that the optimization can be extended to println version too, now that the noformat path doesn't allocate indiscriminantly we can expand _Vprint_unicode_noformat_impl and friends to match _Vprint_unicode_impl and pass the new line flag.

Existing tests already offer enough validation as far as I can tell.
Added test for introduced edge case discovered by the bot. We create a console-backed std::ostream and print a default-constructed empty string_view. Afterwards we check that print/println leave the stream in a good state, then print a marker and verify it appears on the line after the empty println.

Codex was used for local repo setup, PR guideline summaries, searching for relevant tests and evaluating impact outside the scope of the enhancement. It was also used for local benchmarks but they were not satisfying enough to add in the PR since they were very prone to noise. These were the most reliable numbers (sending output to a buffered NUL stream):

Benchmark Old New Result
std::print — long, plain 692 ns 632 ns Improved
std::print — long, escaped at the end 672 ns 677 ns Matching is good
std::println — no escapes 903 ns 834 ns Improved

Copilot AI balanced review requested due to automatic review settings September 25, 2026 22:39
@github-project-automation github-project-automation Bot moved this to Initial Review in STL Code Reviews Sep 25, 2026
@azure-pipelines

This comment was marked as resolved.

This comment was marked as resolved.

@YexuanXiao

This comment has been minimized.

@cristi1990an

Copy link
Copy Markdown
Author

I think we should avoid scanning twice; otherwise, it's not an optimization.

We are. That's why _Find_escape_braces returns the index of where the scan short circuited. Escape braces then copies the characters in the preliminary scan directly

Copilot AI review requested due to automatic review settings September 26, 2026 20:18

This comment was marked as resolved.

Copilot AI review requested due to automatic review settings September 26, 2026 21:12

This comment was marked as resolved.

@cristi1990an

Copy link
Copy Markdown
Author

Another bothering issue I've noticed and I'll investigate in the scope of this PR is the performance cost of calling println instead of print with '\n' as part of the format string. Early benchmarks don't look great:

Run print(text + "\n") println(text) Difference
1 29.34 µs/call 43.43 µs/call +48.1%
2 29.16 µs/call 42.46 µs/call +45.6%

Copilot AI review requested due to automatic review settings September 26, 2026 22:16
@cristi1990an

Copy link
Copy Markdown
Author

Bettter

Run print(text + "\n") println(text) Difference
1 27.44 µs/call 27.62 µs/call +0.6%
2 28.03 µs/call 27.78 µs/call -0.9%

This comment was marked as resolved.

Copilot AI review requested due to automatic review settings September 26, 2026 22:47

This comment was marked as resolved.

Copilot AI review requested due to automatic review settings September 26, 2026 22:55

This comment was marked as resolved.

@YexuanXiao

Copy link
Copy Markdown
Contributor

In my opinion, the optimization in this patch is not worthwhile. For maintainability and consistency, allocating memory here is reasonable. In addition, your patch also introduces new string allocations, which defeats the purpose of the patch. When I previously used GPT to write my character processing program, I also tried something similar, but in the end I gave up on them. AI always tends to assume that the input is mostly ASCII, or to scan for the certain special characters to optimize performance, but they do not help much with performance and greatly complicate the code.

@cristi1990an

Copy link
Copy Markdown
Author

In my opinion, the optimization in this patch is not worthwhile. For maintainability and consistency, allocating memory here is reasonable. In addition, your patch also introduces new string allocations, which defeats the purpose of the patch. When I previously used GPT to write my character processing program, I also tried something similar, but in the end I gave up on them. AI always tends to assume that the input is mostly ASCII, or to scan for the certain special characters to optimize performance, but they do not help much with performance and greatly complicate the code.

The point about complexity is fair, in the sense that enabling the optimizations did require far more rewrites than I've originally imagined. From a bird's eye overview I still consider that the complexity isn't necessarily worse or less manageable compared to the original solution. As a summary of the bulkier changes:

  • majority of code changes are just the result of mirroring new line optimizations already implemented for the format_args_count > zero case. If the implementation effort and code complexity was considered acceptable there, I'd argue these should be considered acceptable for the no formatting args case I'm tackling here too

  • format/print internal machinary is already dense in complexity and edge cases and while this PR doesn't improve that, I can't really say it makes it worse

  • printing stuff is something users do a lot, I see value in having an optimal implementation for it, or at least patch out clear bottlenecks

One thing to clarify, can you pin point the exact scenario in which we would do additional allocations? This should not be the case. We're only cutting possible allocations

@YexuanXiao

Copy link
Copy Markdown
Contributor

You added string _Output_str in _Print_noformat_nonunicode in <ostream>, and I pointed this out in the review https://github.com/microsoft/STL/pull/6465/changes#r4113171834. It seems you are not familiar with the patch.

@cristi1990an

Cornea Cristian (cristi1990an) commented Sep 27, 2026 •

Copy link
Copy Markdown
Author

You added string _Output_str in _Print_noformat_nonunicode in , and I pointed this out in the review https://github.com/microsoft/STL/pull/6465/changes#r4113171834. It seems you are not familiar with the patch.

Initially I would've said that this allocation is me just bringing back a string creation that I've removed higher up in _Print_impl because I was tracking the scenario where _Print_noformat_nonunicode is always called with _Add_newline::_Nope. But under further inspection it seems that we do genuinely have a second allocation taking place if _Unescape_braces was called. So this code println(custom_os, "long string.,.. {{}}") first allocates for escaping braces and then allocates one more time for adding the new line. That second allocation only happens on the non-unicode fallback, if cout is detected as a UTF-8 console, it bypasses _Print_noformat_nonunicode and _Output_str.

I fixed it in the new commit. I now pass _Add_nl to _Unescape_braces so it adds the new line, then pass _Add_newline::_Nope further down.

println(custom_os, "long string...") still needs to allocate because of streambuf design. I would like input on whether any solution for this case can be implemented.

Copilot AI review requested due to automatic review settings September 27, 2026 10:42

This comment was marked as resolved.

Copilot AI review requested due to automatic review settings September 27, 2026 11:36

This comment was marked as resolved.

@StephanTLavavej

This comment was marked as resolved.

@azure-pipelines

This comment was marked as resolved.

@StephanTLavavej Stephan T. Lavavej (StephanTLavavej) changed the title <print>: Optimize no-argument print brace handling <print>: Optimize no-argument print() brace handling Sep 28, 2026
Copilot AI balanced review requested due to automatic review settings October 2, 2026 17:17

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Warning

Copilot couldn't run its full agentic review because it didn't start before the timeout. Make sure your repository has a runner available, or add a copilot-code-review.yml file specifying one with the runs-on attribute. See the docs for more details.

Copilot review overview

Review effort: Lite
Findings: 3 Medium severity · 2 Low severity

Open (5)

Comment thread stl/inc/ostream
#ifdef _CPPRTTI
template <int = 0>
ios_base::iostate _Print_noformat_nonunicode(ostream& _Ostr, const string_view _Str) {
ios_base::iostate _Print_noformat_nonunicode(const _Add_newline _Add_nl, ostream& _Ostr, const string_view _Str) {

@cristi1990an Cornea Cristian (cristi1990an) Oct 2, 2026 •

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I will need some input on this scenario before I tackle this. Original bot comment found this as an issue and I believe it was right. If not, I'm happy to live with this compromise and sacrifice an allocation for this case.

Appending the newline with a separate sputc changes observable streambuf behavior from the previous single sputn of the complete formatted result. For example, a custom buffer that accepts writes via xsputn but whose overflow returns EOF previously accepts println(os, "text"); this path now writes only text, sets badbit, and loses the newline. Preserve a single contiguous write for the non-console ostream newline path.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To clarify, it's not clear to me if two consecutive calls to sputn would be allowed here

Comment thread stl/inc/ostream
Comment on lines +87 to +103
string _Output_str;
string_view _Output = _Str;
if (_Add_nl == _Add_newline::_Yes) {
const size_t _Output_size = _Str.size() + 1;
_Output_str.resize_and_overwrite(_Output_size, [_Str, _Output_size](char* const _Dest_ptr, const size_t) {
if (!_Str.empty()) {
_STD char_traits<char>::copy(_Dest_ptr, _Str.data(), _Str.size());
}
_Dest_ptr[_Str.size()] = '\n';
return _Output_size;
});
_Output = _Output_str;
}

_TRY_IO_BEGIN
const auto _Characters_written = _Ostr.rdbuf()->sputn(_Str.data(), static_cast<streamsize>(_Str.size()));
const bool _Was_insertion_successful = static_cast<size_t>(_Characters_written) == _Str.size();
const auto _Characters_written = _Ostr.rdbuf()->sputn(_Output.data(), static_cast<streamsize>(_Output.size()));
const bool _Was_insertion_successful = static_cast<size_t>(_Characters_written) == _Output.size();
Comment on lines +339 to +366
void test_noformat_console_ostream() {
if constexpr (_Is_ordinary_literal_encoding_utf8()) {
constexpr string_view empty_view{};
constexpr string_view nonempty_view{"ostream println"};
test::win_console test_console{};
FILE* const console_file_stream = test_console.get_file_stream();
filebuf console_file_buffer{console_file_stream};
ostream console_output{&console_file_buffer};

print(console_output, empty_view);
const bool print_set_badbit = console_output.bad();
console_output.clear();

println(console_output, empty_view);
const bool println_set_badbit = console_output.bad();
console_output.clear();

println(console_output, nonempty_view);
const bool nonempty_println_set_badbit = console_output.bad();
console_output.clear();

print(console_output, "ostream marker");

assert(!print_set_badbit && !println_set_badbit && !nonempty_println_set_badbit
&& test_console.get_console_line(0).empty() && test_console.get_console_line(1) == L"ostream println"
&& test_console.get_console_line(2) == L"ostream marker");
}
}

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

print calls pubsync before writing to the console and filebuf::sync flushes the underlying FILE*. It then writes through WriteConsoleW so the console output is available when the function returns

Comment thread stl/inc/ostream
Comment on lines +91 to +96
_Output_str.resize_and_overwrite(_Output_size, [_Str, _Output_size](char* const _Dest_ptr, const size_t) {
if (!_Str.empty()) {
_STD char_traits<char>::copy(_Dest_ptr, _Str.data(), _Str.size());
}
_Dest_ptr[_Str.size()] = '\n';
return _Output_size;
Comment thread stl/inc/print
@StephanTLavavej

This comment was marked as resolved.

@azure-pipelines

This comment was marked as resolved.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

performance Must go faster

Projects

Status: Initial Review

Development

Successfully merging this pull request may close these issues.

4 participants