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));
+ gimple_seq_add_stmt_without_update
+ (&stmts, gimple_build_call (fn_fputc, 2, c, gimple_call_arg (stmt, 3)));
+ 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)