Hi Tomasz,
Thanks for the suggestion to reuse
enable_nonlocking_formatter_optimization as the opt-in gate. Here is
the prototype for patch 4/4, implementing print directly to the
streambuf following the approach of the FILE* overloads in <print>:
- when every argument's formatter sets the trait, std::print writes
directly into the streambuf via _Streambuf_sink (zero-copy into
the put area), instead of formatting to an intermediate string;
- otherwise it falls back to the explicitly-buffered
vprint_nonunicode_buffered function, so behaviour for non-opt-in
formatters is unchanged.
Test results (BABA interleaved, core-pinned, turbo off, serial;
full per-cell tables and methodology at
https://github.com/agicy/std-print-benchmarks):
print(os) vs trunk: 1.04x 1.05x 1.05x 1.19x 1.47x 1.69x
(4B 16B 64B 256B 1024B 4096B; up to 2.9x
for multi-arg output at 4096B)
print fallback path: 1.00x (0.98-1.00, unchanged)
print(FILE*): 1.00x (control, untouched)
print vs format_to: median print/format_to 0.75x -> 1.05x
vprint_nonunicode: 1.04x..1.70x by length (now formats directly
on the branch, mirroring FILE*; no opt-in gate)
format_to: 4.83x total (up to 35.8x at 4096B)
instructions per op: print plain_str/4096 1.51x fewer (perf stat)
A note on the benchmark data: the machine that produced the earlier
numbers is no longer accessible (filesystem corruption on 2026-08-20),
and the benchmark code and raw data on it are lost. The data below was
therefore measured on my personal laptop (i9-12900H) with a freshly
rewritten benchmark using the same methodology (BABA interleaved,
core-pinned, turbo off, serial); the full code and data are at
https://github.com/agicy/std-print-benchmarks. The earlier results
were qualitatively similar, though absolute values may differ slightly
due to hardware variation.
Thanks,
Anlai
>From 133abeee610aeb0195afb8ee9faa6ffd4a042d3f Mon Sep 17 00:00:00 2001
From: Anlai Lu <[email protected]>
Date: Fri, 21 Aug 2026 15:32:16 +0800
Subject: [PATCH] libstdc++: Format directly to ostream in std::print when
formatters opt in
When all formatters set enable_nonlocking_formatter_optimization,
std::print(ostream&, ...) formats zero-copy into the streambuf via
_Streambuf_sink, avoiding the intermediate string. If formatting
throws, partial output may already be in the stream; a standard
change to [ostream.formatted.print] will bless this.
Mirror the FILE* overloads in <print>: vprint_nonunicode formats
directly, buffered variants are vprint_nonunicode_buffered and
vprint_unicode_buffered, and println appends the newline via the
sink instead of concatenating the format string.
libstdc++-v3/ChangeLog:
* include/bits/ostream_print.h (vprint_nonunicode): Format
directly into the streambuf.
(vprint_nonunicode_buffered, vprint_unicode_buffered): New.
* include/std/format (_Streambuf_sink): Add newline mode.
* include/std/ostream (print): Use the direct path when all
formatters opt in.
(println): Write to a newline-appending sink instead of
concatenating the format string.
Signed-off-by: Anlai Lu <[email protected]>
---
libstdc++-v3/include/bits/ostream_print.h | 56 ++++++++++++++---
libstdc++-v3/include/std/format | 12 +++-
libstdc++-v3/include/std/ostream | 76 ++++++++++++++++++++---
3 files changed, 124 insertions(+), 20 deletions(-)
diff --git a/libstdc++-v3/include/bits/ostream_print.h
b/libstdc++-v3/include/bits/ostream_print.h
index 0adf16d4f..1562e5fbe 100644
--- a/libstdc++-v3/include/bits/ostream_print.h
+++ b/libstdc++-v3/include/bits/ostream_print.h
@@ -61,21 +61,19 @@ _GLIBCXX_BEGIN_NAMESPACE_VERSION
ostream::sentry __cerb(__os);
if (__cerb)
{
- __format::_Str_sink<char> __buf;
- std::vformat_to(__buf.out(), __os.getloc(), __fmt, __args);
- auto __out = __buf.view();
-
+ __format::_Streambuf_sink<char> __sink(__os.rdbuf());
__try
{
- std::__ostream_write(__os, __out.data(), __out.size());
+ std::vformat_to(__sink.out(), __os.getloc(), __fmt, __args);
+ __sink._M_finish();
}
__catch(const __cxxabiv1::__forced_unwind&)
{
__os._M_setstate(ios_base::badbit);
__throw_exception_again;
}
- __catch(...)
- { __os._M_setstate(ios_base::badbit); }
+ if (__sink._M_failed())
+ __os.setstate(ios_base::badbit);
}
}
@@ -97,7 +95,7 @@ _GLIBCXX_BEGIN_NAMESPACE_VERSION
void* __open_terminal(streambuf*);
error_code __write_to_terminal(void*, span<char>);
- // If stream refers to a terminal, write a Unicode string to it.
+ // If stream refers to a terminal, write a native Unicode string to it.
if (auto __term = __open_terminal(__os.rdbuf()))
{
#if !defined(_WIN32) || defined(__CYGWIN__)
@@ -138,7 +136,7 @@ _GLIBCXX_BEGIN_NAMESPACE_VERSION
return;
}
- // Otherwise just insert the string as vprint_nonunicode does.
+ // Otherwise just write the string to the stream.
__try
{
std::__ostream_write(__os, __out.data(), __out.size());
@@ -153,6 +151,46 @@ _GLIBCXX_BEGIN_NAMESPACE_VERSION
}
#endif // _WIN32
}
+
+ _GLIBCXX_PRINT_INLINE_USED
+ inline void
+ vprint_nonunicode_buffered(ostream& __os, string_view __fmt, format_args
__args)
+ {
+ ostream::sentry __cerb(__os);
+ if (__cerb)
+ {
+ __format::_Str_sink<char> __buf;
+ std::vformat_to(__buf.out(), __os.getloc(), __fmt, __args);
+ auto __out = __buf.view();
+
+ __try
+ {
+ std::__ostream_write(__os, __out.data(), __out.size());
+ }
+ __catch(const __cxxabiv1::__forced_unwind&)
+ {
+ __os._M_setstate(ios_base::badbit);
+ __throw_exception_again;
+ }
+ __catch(...)
+ { __os._M_setstate(ios_base::badbit); }
+ }
+ }
+
+ _GLIBCXX_PRINT_INLINE_USED
+ inline void
+ vprint_unicode_buffered(ostream& __os, string_view __fmt, format_args __args)
+ {
+#if !defined(_WIN32) || defined(__CYGWIN__)
+ // For most targets we don't need to do anything special to write
+ // Unicode to a terminal. Just use the nonunicode function.
+ std::vprint_nonunicode_buffered(__os, __fmt, __args);
+#else
+ // On Windows vprint_unicode formats to a string first anyway.
+ // Just use that.
+ std::vprint_unicode(__os, __fmt, __args);
+#endif
+ }
#undef _GLIBCXX_PRINT_INLINE_USED
_GLIBCXX_END_NAMESPACE_VERSION
diff --git a/libstdc++-v3/include/std/format b/libstdc++-v3/include/std/format
index 39f18b34f..4e37ddb32 100644
--- a/libstdc++-v3/include/std/format
+++ b/libstdc++-v3/include/std/format
@@ -3498,6 +3498,7 @@ namespace __format
protected:
basic_streambuf<_CharT, _Traits>* _M_sbuf;
bool _M_write_failed = false;
+ bool _M_append_newline = false;
bool
_M_on_stack() const noexcept
@@ -3557,8 +3558,9 @@ namespace __format
public:
[[__gnu__::__always_inline__]]
explicit
- _Streambuf_sink(basic_streambuf<_CharT, _Traits>* __sbuf) noexcept
- : _M_sbuf(__sbuf)
+ _Streambuf_sink(basic_streambuf<_CharT, _Traits>* __sbuf,
+ bool __append_newline = false) noexcept
+ : _M_sbuf(__sbuf), _M_append_newline(__append_newline)
{ _M_reserve_put_area(); }
// _M_bump is not overridden: the default advances _M_next within
@@ -3593,7 +3595,11 @@ namespace __format
void
_M_finish()
- { _M_flush(); }
+ {
+ if (_M_append_newline)
+ this->_M_write(_CharT('\n'));
+ _M_flush();
+ }
bool
_M_failed() const noexcept
diff --git a/libstdc++-v3/include/std/ostream b/libstdc++-v3/include/std/ostream
index b99c0f92d..0abc35352 100644
--- a/libstdc++-v3/include/std/ostream
+++ b/libstdc++-v3/include/std/ostream
@@ -167,34 +167,94 @@ _GLIBCXX_BEGIN_NAMESPACE_VERSION
void
vprint_unicode(ostream& __os, string_view __fmt, format_args __args);
+ void
+ vprint_nonunicode_buffered(ostream& __os, string_view __fmt, format_args
__args);
+
+ void
+ vprint_unicode_buffered(ostream& __os, string_view __fmt, format_args
__args);
+
template<typename... _Args>
inline void
print(ostream& __os, format_string<_Args...> __fmt, _Args&&... __args)
{
+ constexpr bool __locksafe =
+ (enable_nonlocking_formatter_optimization<remove_cvref_t<_Args>> &&
...);
+
auto __fmtargs = std::make_format_args(__args...);
#if defined(_WIN32) && !defined(__CYGWIN__)
if constexpr (__unicode::__literal_encoding_is_utf8())
- std::vprint_unicode(__os, __fmt.get(), __fmtargs);
+ std::vprint_unicode_buffered(__os, __fmt.get(), __fmtargs);
else
#endif
+ if constexpr (__locksafe)
std::vprint_nonunicode(__os, __fmt.get(), __fmtargs);
+ else
+ std::vprint_nonunicode_buffered(__os, __fmt.get(), __fmtargs);
}
template<typename... _Args>
inline void
println(ostream& __os, format_string<_Args...> __fmt, _Args&&... __args)
{
+ constexpr bool __locksafe =
+ (enable_nonlocking_formatter_optimization<remove_cvref_t<_Args>> &&
...);
+
+ // Avoid the string concatenation in print(os, dynamic_format(...)).
auto __fmtargs = std::make_format_args(__args...);
- std::string __fmtn;
- __fmtn.reserve(__fmt.get().size() + 1);
- __fmtn = __fmt.get();
- __fmtn += '\n';
+
#if defined(_WIN32) && !defined(__CYGWIN__)
if constexpr (__unicode::__literal_encoding_is_utf8())
- std::vprint_unicode(__os, __fmtn, __fmtargs);
+ {
+ // Can't avoid the concatenation; call vprint_unicode_buffered
directly.
+ string __fmtn;
+ __fmtn.reserve(__fmt.get().size() + 1);
+ __fmtn = __fmt.get();
+ __fmtn += '\n';
+ std::vprint_unicode_buffered(__os, __fmtn, __fmtargs);
+ }
else
#endif
- std::vprint_nonunicode(__os, __fmtn, __fmtargs);
+ if (ostream::sentry __cerb{__os})
+ {
+ // Use a newline-appending sink, as the FILE* overloads do.
+ if constexpr (__locksafe)
+ {
+ __format::_Streambuf_sink<char> __sink(__os.rdbuf(), true);
+ __try
+ {
+ std::vformat_to(__sink.out(), __os.getloc(), __fmt.get(),
+ __fmtargs);
+ __sink._M_finish();
+ }
+ __catch(const __cxxabiv1::__forced_unwind&)
+ {
+ __os._M_setstate(ios_base::badbit);
+ __throw_exception_again;
+ }
+ if (__sink._M_failed())
+ __os.setstate(ios_base::badbit);
+ }
+ else
+ {
+ // Format to a string, then write via the newline-appending sink.
+ __format::_Str_sink<char> __buf;
+ std::vformat_to(__buf.out(), __os.getloc(), __fmt.get(),
__fmtargs);
+ string_view __s(__buf.view());
+ __format::_Streambuf_sink<char> __sink(__os.rdbuf(), true);
+ __try
+ {
+ __sink.out() = __s;
+ __sink._M_finish();
+ }
+ __catch(const __cxxabiv1::__forced_unwind&)
+ {
+ __os._M_setstate(ios_base::badbit);
+ __throw_exception_again;
+ }
+ if (__sink._M_failed())
+ __os.setstate(ios_base::badbit);
+ }
+ }
}
// Defined for C++26, supported as an extension to C++23.
@@ -202,7 +262,7 @@ _GLIBCXX_BEGIN_NAMESPACE_VERSION
{
#if defined(_WIN32) && !defined(__CYGWIN__)
if constexpr (__unicode::__literal_encoding_is_utf8())
- std::vprint_unicode(__os, "\n", std::make_format_args());
+ std::vprint_unicode_buffered(__os, "\n", std::make_format_args());
else
#endif
__os.put('\n');
--
2.55.0