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

Reply via email to