[llvm] [flang-rt][runtime] Optimize formatted writes by reducing ExternalFileUnit::Emit() calls (PR #222522)

Kaviya Rajendiran via llvm-commits llvm-commits at lists.llvm.org
Wed Sep 9 23:50:15 PDT 2026


https://github.com/kaviya2510 created https://github.com/llvm/llvm-project/pull/222522

For formatted writes in flang, the runtime spends most of the time  in `Fortran::runtime::io::ExternalFileUnit::Emit()`, which copies formatted output into the unit's internal buffer. For a formatted write such as `"(T2,A2,1X,3F20.10)"`, a single output line of roughly 65 characters required around 39 separate `ExternalFileUnit::Emit()` calls.

This implementation reduced the number of ExternalFileUnit::Emit() calls by grouping output data before writing it to the buffer.

- Formatted numeric output is now assembled into a temporary buffer before being emitted. Previously, a value formatted with F20.10 is written as multiple small pieces, such as padding spaces, sign, digits, decimal point, and fractional part each triggering separate `ExternalFileUnit::Emit()` calls. Now these components are combined and written in a single call.
- Runs of repeated characters (such as spaces or zeros) are now grouped together before being written. Previously, a sequence of eight spaces was handled as eight separate `ExternalFileUnit::Emit()` calls, with each space triggering its own call. With this change, the sequence is buffered and emitted in a single `ExternalFileUnit::Emit()` call. For longer runs, the data is emitted in 64-byte chunks. This reduces the overhead associated with a large number of small write.

>From 5a77f846c7a78399e9a47cac2cb3b39bb60a6d4e Mon Sep 17 00:00:00 2001
From: Kaviya Rajendiran <kaviyara2000 at gmail.com>
Date: Thu, 10 Sep 2026 12:04:50 +0530
Subject: [PATCH] [flang-rt][runtime] Optimize formatted writes by reducing
 ExternalFileUnit::Emit() calls

---
 .../include/flang-rt/runtime/emit-encoded.h   | 13 +++-
 flang-rt/lib/runtime/edit-output.cpp          | 35 +++++++++++
 flang-rt/unittests/Runtime/ExternalIOTest.cpp | 40 ++++++++++++
 .../unittests/Runtime/NumericalFormatTest.cpp | 62 +++++++++++++++++++
 4 files changed, 147 insertions(+), 3 deletions(-)

diff --git a/flang-rt/include/flang-rt/runtime/emit-encoded.h b/flang-rt/include/flang-rt/runtime/emit-encoded.h
index ea83901fcc8be..ca0fccd583ac4 100644
--- a/flang-rt/include/flang-rt/runtime/emit-encoded.h
+++ b/flang-rt/include/flang-rt/runtime/emit-encoded.h
@@ -97,11 +97,18 @@ RT_API_ATTRS bool EmitRepeated(CONTEXT &to, char ch, std::size_t n) {
   ConnectionState &connection{to.GetConnectionState()};
   if (connection.internalIoCharKind <= 1 &&
       connection.access != Access::Stream) {
-    // faster path, no encoding needed
-    while (n-- > 0) {
-      if (!to.Emit(&ch, 1)) {
+    // Faster path, no encoding needed: emit the run in bulk
+    char buffer[64];
+    std::size_t chunk{n < sizeof buffer ? n : sizeof buffer};
+    for (std::size_t j{0}; j < chunk; ++j) {
+      buffer[j] = ch;
+    }
+    while (n > 0) {
+      std::size_t bytes{n < chunk ? n : chunk};
+      if (!to.Emit(buffer, bytes)) {
         return false;
       }
+      n -= bytes;
     }
   } else {
     while (n-- > 0) {
diff --git a/flang-rt/lib/runtime/edit-output.cpp b/flang-rt/lib/runtime/edit-output.cpp
index af2dc0a036687..7e52be9b22051 100644
--- a/flang-rt/lib/runtime/edit-output.cpp
+++ b/flang-rt/lib/runtime/edit-output.cpp
@@ -567,6 +567,41 @@ RT_API_ATTRS bool RealOutputEditing<KIND>::EditFOutput(const DataEdit &edit) {
       zeroesBeforePoint = 1;
       ++totalLength;
     }
+    int leadingSpaces{width > totalLength ? width - totalLength : 0};
+    // Fast path: assemble the whole field (padding, sign, digits, point,
+    // fractional zeroes, trailing blanks) into one buffer and emit it with a
+    // single Emit() call.
+    constexpr int maxAssembledField{512};
+    if (!edit.IsListDirected() &&
+        leadingSpaces + totalLength <= maxAssembledField) {
+      char field[maxAssembledField];
+      char *p{field};
+      auto copyFrom{[&](const char *from, int n) {
+        while (n-- > 0) {
+          *p++ = *from++;
+        }
+      }};
+      auto fillWith{[&](char c, int n) {
+        while (n-- > 0) {
+          *p++ = c;
+        }
+      }};
+      fillWith(' ', leadingSpaces);
+      copyFrom(convertedStr, signLength + digitsBeforePoint);
+      fillWith('0', zeroesBeforePoint);
+      *p++ = edit.modes.editingFlags & decimalComma ? ',' : '.';
+      fillWith('0', zeroesAfterPoint);
+      copyFrom(convertedStr + signLength + digitsBeforePoint, digitsAfterPoint);
+      fillWith('0', trailingZeroes);
+      fillWith(' ', trailingBlanks_);
+      // Safety check: the number of characters wrote (p - field)
+      // must equal the field width  computed (leadingSpaces + totalLength).
+      // If they differ, the buffer was mis-filled, so stop instead of
+      // emitting a wrong-length field.
+      RUNTIME_CHECK(
+          io_.GetIoErrorHandler(), p - field == leadingSpaces + totalLength);
+      return EmitAscii(io_, field, static_cast<std::size_t>(p - field));
+    }
     return EmitPrefix(edit, totalLength, width) &&
         EmitAscii(io_, convertedStr, signLength + digitsBeforePoint) &&
         EmitRepeated(io_, '0', zeroesBeforePoint) &&
diff --git a/flang-rt/unittests/Runtime/ExternalIOTest.cpp b/flang-rt/unittests/Runtime/ExternalIOTest.cpp
index 1fe3b08126c5d..e60f59289838b 100644
--- a/flang-rt/unittests/Runtime/ExternalIOTest.cpp
+++ b/flang-rt/unittests/Runtime/ExternalIOTest.cpp
@@ -383,6 +383,46 @@ TEST(ExternalIOTests, TestDirectFormatted) {
       << "EndIoStatement() for Close";
 }
 
+// A numeric output field, and its blank or asterisk padding, are emitted in
+// bulk.  Writing a field that does not fit in a fixed-size record must still
+// be diagnosed as a record overrun rather than running past the record.
+TEST(ExternalIOTests, TestFormattedFixedRecordOverrun) {
+  // OPEN(NEWUNIT=unit,ACCESS='SEQUENTIAL',ACTION='WRITE',&
+  //   FORM='FORMATTED',RECL=10,STATUS='SCRATCH')
+  auto *io{IONAME(BeginOpenNewUnit)(__FILE__, __LINE__)};
+  ASSERT_TRUE(IONAME(SetAccess)(io, "SEQUENTIAL", 10))
+      << "SetAccess(SEQUENTIAL)";
+  ASSERT_TRUE(IONAME(SetAction)(io, "WRITE", 5)) << "SetAction(WRITE)";
+  ASSERT_TRUE(IONAME(SetForm)(io, "FORMATTED", 9)) << "SetForm(FORMATTED)";
+  static constexpr std::size_t recl{10};
+  ASSERT_TRUE(IONAME(SetRecl)(io, recl)) << "SetRecl()";
+  ASSERT_TRUE(IONAME(SetStatus)(io, "SCRATCH", 7)) << "SetStatus(SCRATCH)";
+
+  int unit{-1};
+  ASSERT_TRUE(IONAME(GetNewUnit)(io, unit)) << "GetNewUnit()";
+  ASSERT_EQ(IONAME(EndIoStatement)(io), IostatOk)
+      << "EndIoStatement() for OpenNewUnit";
+
+  auto writeReal{[&](const char *format, double x) {
+    io = IONAME(BeginExternalFormattedOutput)(
+        format, std::strlen(format), nullptr, unit, __FILE__, __LINE__);
+    IONAME(EnableHandlers)(io, true /*IOSTAT=*/);
+    IONAME(OutputReal64)(io, x);
+    return IONAME(EndIoStatement)(io);
+  }};
+
+  EXPECT_EQ(writeReal("(F10.4)", 3.25), IostatOk) << "F10.4 into RECL=10";
+  EXPECT_EQ(writeReal("(F20.10)", 3.25), IostatRecordWriteOverrun)
+      << "F20.10 into RECL=10";
+  EXPECT_EQ(writeReal("(F12.3)", 1.0e300), IostatRecordWriteOverrun)
+      << "F12.3 overflow into RECL=10";
+
+  io = IONAME(BeginClose)(unit, __FILE__, __LINE__);
+  ASSERT_TRUE(IONAME(SetStatus)(io, "DELETE", 6)) << "SetStatus(DELETE)";
+  ASSERT_EQ(IONAME(EndIoStatement)(io), IostatOk)
+      << "EndIoStatement() for Close";
+}
+
 TEST(ExternalIOTests, TestSequentialVariableFormatted) {
   // OPEN(NEWUNIT=unit,ACCESS='SEQUENTIAL',ACTION='READWRITE',&
   //   FORM='FORMATTED',STATUS='SCRATCH')
diff --git a/flang-rt/unittests/Runtime/NumericalFormatTest.cpp b/flang-rt/unittests/Runtime/NumericalFormatTest.cpp
index 6af17e99418b4..2ab8c843ec6f6 100644
--- a/flang-rt/unittests/Runtime/NumericalFormatTest.cpp
+++ b/flang-rt/unittests/Runtime/NumericalFormatTest.cpp
@@ -1083,3 +1083,65 @@ TEST(IOApiTests, ATEditDescriptorOutput) {
         << "Multiple AT test: expected 'abc def#' got '" << got << "'";
   }
 }
+
+// Real output fields and their blank/zero padding are emitted in bulk rather
+// than one piece (or one character) at a time.  The resulting text must not
+// depend on how the field is broken up internally, so exercise a range of
+// field widths that straddles those internal boundaries, along with the modes
+// and descriptors that contribute pieces to a field.
+TEST(IOApiTests, RealOutputFieldPadding) {
+  // Leading blank padding, from a field that barely fits its value up to one
+  // too wide to be built in a single fixed-size buffer.
+  for (int width :
+      {5, 12, 60, 63, 64, 65, 127, 128, 129, 511, 512, 513, 514, 600}) {
+    std::string format{"(F" + std::to_string(width) + ".2)"};
+    std::string expect{std::string(width - 4, ' ') + "3.25"};
+    std::string got;
+    EXPECT_TRUE(CompareFormatReal(format.c_str(), 3.25, expect.c_str(), got))
+        << "'" << format << "': expected '" << expect << "', got '" << got
+        << "'";
+  }
+
+  // A value too large for its field is replaced by a field of asterisks.
+  for (int width : {5, 63, 64, 65, 129}) {
+    std::string format{"(F" + std::to_string(width) + ".3)"};
+    std::string expect(width, '*');
+    std::string got;
+    EXPECT_TRUE(CompareFormatReal(format.c_str(), 1.0e300, expect.c_str(), got))
+        << "'" << format << "': expected " << width << " asterisks, got '"
+        << got << "'";
+  }
+
+  // Trailing blanks, decimal comma, sign control and a processor-selected
+  // width all contribute to the field.  A '#' marker pins the field's right
+  // edge so that trailing blanks are verified rather than stripped.
+  using TestCaseTy = std::tuple<const char *, double, std::string>;
+  const std::vector<TestCaseTy> testCases{
+      {"(E140.10)", 1.0, std::string(124, ' ') + "0.1000000000E+01"},
+      {"(DC,F10.3)", 3.25, "     3,250"},
+      {"(SP,F12.4)", 3.25, "     +3.2500"},
+      {"(F0.4)", 3.25, "3.2500"},
+      {"(G14.4,'#')", 3.25, "     3.250    #"},
+  };
+  for (auto const &[format, value, expect] : testCases) {
+    std::string got;
+    EXPECT_TRUE(CompareFormatReal(format, value, expect.c_str(), got))
+        << "'" << format << "': expected '" << expect << "', got '" << got
+        << "'";
+  }
+
+  // List-directed real output is prefixed by a leading blank and its prefix
+  // decides whether the record must advance first, so it is not assembled
+  // together with the value.
+  {
+    char buffer[32];
+    auto cookie{IONAME(BeginInternalListOutput)(buffer, sizeof buffer)};
+    EXPECT_TRUE(IONAME(OutputReal64)(cookie, 3.25));
+    auto status{IONAME(EndIoStatement)(cookie)};
+    EXPECT_EQ(status, 0);
+    EXPECT_TRUE(
+        CompareFormattedStrings(" 3.25", std::string{buffer, sizeof buffer}))
+        << "list-directed real: got '" << std::string{buffer, sizeof buffer}
+        << "'";
+  }
+}



More information about the llvm-commits mailing list