[gcc r17-3505] gimple-fold: fold fwrite of a single byte to fputc

Kyrylo Tkachov via Gcc-cvs <[email protected]>
Newsgroups gmane.comp.gcc.cvs
Message-ID <[email protected]>
https://gcc.gnu.org/g:dc6b71d23465f237165050bc30c4e038c602a2d7

commit r17-3505-gdc6b71d23465f237165050bc30c4e038c602a2d7
Author: Kyrylo Tkachov <[email protected]>
Date:   Fri Aug 14 17:04:39 2026 -0700

    gimple-fold: fold fwrite of a single byte to fputc
    
    fwrite (p, 1, 1, f) transfers one byte and, when nothing looks at its result,
    writes exactly what fputc (*p, f) writes.  fputc reaches the stream directly
    while fwrite goes through the generic buffered-write path, so the fold is
    worth making.  GCC already treats the two as interchangeable in the other
    direction, since gimple_fold_builtin_fputs turns a one-character fputs into
    fputc and a longer one into fwrite.  There was simply no folder for fwrite.
    
    The unlocked entry points fold the same way, into fputc_unlocked, on the
    assumption gimple_fold_builtin_fputs already makes: a program that calls one
    unlocked stdio function has the others available too.
    
    jq emits its output one character at a time through a helper that ends in
    fwrite (&c, 1, 1, f), which is the shape this fold is aimed at:
    
        before                              after
            stp     x29, x30, [sp, -32]!    and     w0, w0, 255
            mov     w2, 1                   b       fputc
            mov     x29, sp
            mov     x3, x1
            uxtw    x1, w2
            strb    w0, [sp, 31]
            add     x0, sp, 31
            bl      fwrite
            ldp     x29, x30, [sp], 32
            ret
    
    Measured with jq 1.8.2 over a 57 MB NDJSON corpus on Grace with
    -mcpu=grace -O3 gets these speedups.
    
        jq -c .                             -30.30% instructions, -44.80% cycles
        jq -c -f <a map/select filter>       -2.92% instructions,  -5.67% cycles
    
    Bootstrapped and tested on aarch64-none-linux-gnu.
    
    gcc/ChangeLog:
    
            * gimple-fold.cc (gimple_fold_builtin_fwrite): New function.
            (gimple_fold_builtin): Call it for BUILT_IN_FWRITE and
            BUILT_IN_FWRITE_UNLOCKED.
    
    gcc/testsuite/ChangeLog:
    
            * gcc.dg/fwrite-1.c: New test.
            * gcc.dg/fwrite-2.c: New test.
            * gcc.dg/fwrite-3.c: New test.
    
    Signed-off-by: Kyrylo Tkachov <[email protected]>

Diff:
---
 gcc/gimple-fold.cc              | 46 +++++++++++++++++++++++++++++++++++++++++
 gcc/testsuite/gcc.dg/fwrite-1.c | 15 ++++++++++++++
 gcc/testsuite/gcc.dg/fwrite-2.c | 32 ++++++++++++++++++++++++++++
 gcc/testsuite/gcc.dg/fwrite-3.c | 30 +++++++++++++++++++++++++++
 4 files changed, 123 insertions(+)

diff --git a/gcc/gimple-fold.cc b/gcc/gimple-fold.cc
index 24041466108f..17b65e0a9e92 100644
--- a/gcc/gimple-fold.cc
+++ b/gcc/gimple-fold.cc
@@ -3099,6 +3099,48 @@ gimple_fold_builtin_fputs (gimple_stmt_iterator *gsi,
     }
 }
 
+/* Fold a call to fwrite (PTR, SIZE, N, STREAM) at *GSI.  UNLOCKED says whether
+   the callee is fwrite_unlocked rather than fwrite.  A call that transfers a
+   single byte and whose result is nobody's business writes the same byte as
+   fputc (*PTR, STREAM), which reaches the stream without going through the
+   generic buffered-write path.  Return true if the call was folded.  */
+
+static bool
+gimple_fold_builtin_fwrite (gimple_stmt_iterator *gsi, bool unlocked)
+{
+  gimple *stmt = gsi_stmt (*gsi);
+
+  /* fwrite reports the number of items transferred and fputc the character
+     written, so only fold when nothing looks at the result.  */
+  if (gimple_call_lhs (stmt))
+    return false;
+
+  /* fwrite transfers SIZE * N bytes, so writing a single byte needs both
+     counts to be one: no other pair of non-negative values multiplies to
+     one.  */
+  if (!integer_onep (gimple_call_arg (stmt, 1))
+      || !integer_onep (gimple_call_arg (stmt, 2)))
+    return false;
+
+  /* If we're using an unlocked function, assume the other unlocked
+     functions exist explicitly.  */
+  tree const fn_fputc = (unlocked
+			 ? builtin_decl_explicit (BUILT_IN_FPUTC_UNLOCKED)
+			 : builtin_decl_implicit (BUILT_IN_FPUTC));
+  if (!fn_fputc || (!gimple_vdef (stmt) && gimple_in_ssa_p (cfun)))
+    return false;
+
+  location_t loc = gimple_location (stmt);
+  gimple_seq stmts = NULL;
+  tree byte = gimple_load_first_char (loc, gimple_call_arg (stmt, 0), &stmts);
+  tree c = gimple_convert (&stmts, integer_type_node, byte);
+  tree stream = gimple_call_arg (stmt, 3);
+  gcall *repl = gimple_build_call (fn_fputc, 2, c, stream);
+  gimple_seq_add_stmt_without_update (&stmts, repl);
+  gsi_replace_with_seq_vops (gsi, stmts);
+  return true;
+}
+
 /* Fold a call to the __mem{cpy,pcpy,move,set}_chk builtin.
    DEST, SRC, LEN, and SIZE are the arguments to the call.
    IGNORE is true, if return value can be ignored.  FCODE is the BUILT_IN_*
@@ -5583,6 +5625,10 @@ gimple_fold_builtin (gimple_stmt_iterator *gsi)
     case BUILT_IN_FPUTS_UNLOCKED:
       return gimple_fold_builtin_fputs (gsi, gimple_call_arg (stmt, 0),
 					gimple_call_arg (stmt, 1), true);
+    case BUILT_IN_FWRITE:
+      return gimple_fold_builtin_fwrite (gsi, false);
+    case BUILT_IN_FWRITE_UNLOCKED:
+      return gimple_fold_builtin_fwrite (gsi, true);
     case BUILT_IN_MEMCPY_CHK:
     case BUILT_IN_MEMPCPY_CHK:
     case BUILT_IN_MEMMOVE_CHK:
diff --git a/gcc/testsuite/gcc.dg/fwrite-1.c b/gcc/testsuite/gcc.dg/fwrite-1.c
new file mode 100644
index 000000000000..5e3a5043e9af
--- /dev/null
+++ b/gcc/testsuite/gcc.dg/fwrite-1.c
@@ -0,0 +1,15 @@
+/* fwrite of a single byte whose result is unused writes the same byte as
+   fputc, so it should be folded.  */
+/* { dg-do compile } */
+/* { dg-options "-O2 -Wno-unused-result -fdump-tree-optimized" } */
+
+#include <stdio.h>
+
+void one (FILE *f, const char *p) { fwrite (p, 1, 1, f); }
+size_t used (FILE *f, const char *p) { return fwrite (p, 1, 1, f); }
+void two_items (FILE *f, const char *p) { fwrite (p, 1, 2, f); }
+void two_bytes (FILE *f, const char *p) { fwrite (p, 2, 1, f); }
+void unknown (FILE *f, const char *p, size_t n) { fwrite (p, 1, n, f); }
+
+/* { dg-final { scan-tree-dump-times "fputc" 1 "optimized" } } */
+/* { dg-final { scan-tree-dump-times "fwrite" 4 "optimized" } } */
diff --git a/gcc/testsuite/gcc.dg/fwrite-2.c b/gcc/testsuite/gcc.dg/fwrite-2.c
new file mode 100644
index 000000000000..7565f52def8b
--- /dev/null
+++ b/gcc/testsuite/gcc.dg/fwrite-2.c
@@ -0,0 +1,32 @@
+/* Check that folding fwrite of a single byte to fputc keeps the output the
+   same, including the bytes that are not folded, and that a side effect in the
+   stream argument is still evaluated exactly once.  */
+/* { dg-do run } */
+/* { dg-options "-O2 -Wno-unused-result" } */
+
+#include <stdio.h>
+
+extern void abort (void);
+
+const char s[] = "abcd";
+
+int
+main (void)
+{
+  FILE *streams[] = { stdout, NULL }, **p = streams;
+
+  fwrite (s, 1, 1, stdout);
+  fwrite (s + 1, 1, 1, stdout);
+  fwrite (s + 2, 1, 2, stdout);
+  fwrite (s, 2, 1, stdout);
+
+  /* The folded call must still advance P exactly once.  */
+  fwrite (s, 1, 1, *p++);
+  if (p != streams + 1 || *p != NULL)
+    abort ();
+
+  fflush (stdout);
+  return 0;
+}
+
+/* { dg-output "abcdaba" } */
diff --git a/gcc/testsuite/gcc.dg/fwrite-3.c b/gcc/testsuite/gcc.dg/fwrite-3.c
new file mode 100644
index 000000000000..0fb9d0a234fb
--- /dev/null
+++ b/gcc/testsuite/gcc.dg/fwrite-3.c
@@ -0,0 +1,30 @@
+/* fwrite_unlocked of a single byte whose result is unused writes the same byte
+   as fputc_unlocked, so it is folded the same way as the locked form, and into
+   the unlocked entry point rather than the locked one.  */
+/* { dg-do compile } */
+/* { dg-options "-O2 -Wno-unused-result -fdump-tree-optimized" } */
+
+#include <stdio.h>
+
+/* glibc defines fwrite_unlocked as a macro that turns a small constant
+   transfer into a putc_unlocked loop, which would keep the call from ever
+   reaching the folder under test.  */
+#undef fwrite_unlocked
+
+/* Declared here rather than through _GNU_SOURCE so that the test does not
+   depend on the host header exposing the unlocked entry points.  */
+extern int (fputc_unlocked) (int, FILE *);
+extern size_t (fwrite_unlocked) (const void *, size_t, size_t, FILE *);
+
+void one (FILE *f, const char *p) { fwrite_unlocked (p, 1, 1, f); }
+size_t used (FILE *f, const char *p) { return fwrite_unlocked (p, 1, 1, f); }
+void two_items (FILE *f, const char *p) { fwrite_unlocked (p, 1, 2, f); }
+void two_bytes (FILE *f, const char *p) { fwrite_unlocked (p, 2, 1, f); }
+void unknown (FILE *f, const char *p, size_t n) { fwrite_unlocked (p, 1, n, f); }
+
+/* { dg-final { scan-tree-dump-times "fputc_unlocked" 1 "optimized" } } */
+/* { dg-final { scan-tree-dump-times "fwrite_unlocked" 4 "optimized" } } */
+/* The unlocked form must not be folded into the locked fputc.  A call with no
+   result is dumped as "  fputc (...", which the leading space matches without
+   also matching fputc_unlocked.  */
+/* { dg-final { scan-tree-dump-not " fputc \\(" "optimized" } } */
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.