Re: [PATCH] gimple-fold: fold fwrite of a single byte to fputc
Richard Biener <[email protected]>
| Newsgroups | gmane.comp.gcc.patches |
|---|---|
| Message-ID | <CAFiYyc2F6VeZ5CCZBevybeHEcBNwf3jx+rmqVHe1EiAWDkW0hQ@mail.gmail.com> |
On Wed, Aug 19, 2026 at 1:03 PM <[email protected]> wrote: > > From: Kyrylo Tkachov <[email protected]> > > 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. > Ok for trunk? > Thanks, > Kyrill > > 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]> > --- > gcc/gimple-fold.cc | 47 +++++++++++++++++++++++++++++++++ > gcc/testsuite/gcc.dg/fwrite-1.c | 15 +++++++++++ > gcc/testsuite/gcc.dg/fwrite-2.c | 32 ++++++++++++++++++++++ > gcc/testsuite/gcc.dg/fwrite-3.c | 25 ++++++++++++++++++ > 4 files changed, 119 insertions(+) > create mode 100644 gcc/testsuite/gcc.dg/fwrite-1.c > create mode 100644 gcc/testsuite/gcc.dg/fwrite-2.c > create mode 100644 gcc/testsuite/gcc.dg/fwrite-3.c > > diff --git a/gcc/gimple-fold.cc b/gcc/gimple-fold.cc > index 24041466108..583afe119e7 100644 > --- a/gcc/gimple-fold.cc > +++ b/gcc/gimple-fold.cc > @@ -3099,6 +3099,49 @@ 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 = make_ssa_name (integer_type_node); > + gimple_seq_add_stmt_without_update (&stmts, > + gimple_build_assign (c, NOP_EXPR, byte)); tree c = gimple_convert (&stmts, integer_type_node, byte); > + gimple_seq_add_stmt_without_update > + (&stmts, gimple_build_call (fn_fputc, 2, c, gimple_call_arg (stmt, 3))); gimple_build (&stmts, fn_putc, void_type_node, c, gimple_call_arg (stmt, 3)); might work here (unsure about calls with no LHS). > + 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 +5626,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 00000000000..5e3a5043e9a > --- /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 00000000000..7565f52def8 > --- /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 00000000000..6ee0f58cb59 > --- /dev/null > +++ b/gcc/testsuite/gcc.dg/fwrite-3.c > @@ -0,0 +1,25 @@ > +/* 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> > + > +/* 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" } } */ > -- > 2.50.1 (Apple Git-155) >