[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" } } */