[PATCH 4/4] libstdc++: Optimize std::print using _Streambuf_sink

Anlai Lu agicy@qq.com
Sun Aug 23 11:25:40 GMT 2026


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 <agicy@qq.com>
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 <agicy@qq.com>
---
 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



More information about the Libstdc++ mailing list