> On 21 Aug 2026, at 13:04, Richard Biener <[email protected]> wrote: > > On Wed, 19 Aug 2026, [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 | 45 +++++++++++++++++++++++++++++++++ >> 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, 122 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..a3b88cf719c 100644 >> --- a/gcc/gimple-fold.cc >> +++ b/gcc/gimple-fold.cc >> @@ -3099,6 +3099,47 @@ 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); >> + gimple_seq_add_stmt_without_update >> + (&stmts, gimple_build_call (fn_fputc, 2, c, gimple_call_arg (stmt, 3))); > > So I take the gimple_build for this didn't work out. But can you please > use temporary variables to avoid such ugly formatting?
Sorry, I should have been explicit that it didn’t work out.
>
> OK with such change.
Thanks, here is what I’m pushing to master.
Kyrill
> Thanks,
> Richard.
>
>> + 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 +5624,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..0fb9d0a234f
>> --- /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" } } */
>>
>
> --
> Richard Biener <[email protected]>
> SUSE Software Solutions Germany GmbH,
> Frankenstrasse 146, 90461 Nuernberg, Germany;
> GF: Jochen Jaser, Andrew McDonald, Abhinav Puri; (HRB 36809, AG Nuernberg)
0001-gimple-fold-fold-fwrite-of-a-single-byte-to-fputc.patch
Description: 0001-gimple-fold-fold-fwrite-of-a-single-byte-to-fputc.patch
