> On 19 Aug 2026, at 14:06, Richard Biener <[email protected]> wrote:
> 
> 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);

Will do, thanks.

> 
>> +  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).

There's no gimple_build overload taking a function decl so that wouldn’t 
compile, unless I’m not looking in the right place.
Thanks,
Kyrill

> 
>> +  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)


Reply via email to