[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