On a AMD Ryzen 7 3700X system:

    $ timeout 10 taskset 1 ./src/cat-prev /dev/zero \
        | taskset 2 pv -r > /dev/null
    [1.84GiB/s]
    $ timeout 10 taskset 1 ./src/cat /dev/zero \
        | taskset 2 pv -r > /dev/null
    [7.92GiB/s]

* NEWS: Mention the improvement.
* src/cat.c: Include isapipe.h, splice.h, and unistd--.h.
(splice_cat): New function.
(main): Use it.
* src/local.mk (noinst_HEADERS): Add src/splice.h.
* src/splice.h: New file, based on definitions from src/yes.c.
* src/yes.c: Include splice.h.
(pipe_splice_size): Use increase_pipe_size from src/splice.h.
(SPLICE_PIPE_SIZE): Remove definition, moved to src/splice.h.
* tests/cat/splice.sh: New file, based on some tests in
tests/misc/yes.sh.
* tests/local.mk (all_tests): Add the new test.
---
 NEWS                |  4 ++
 src/cat.c           | 98 +++++++++++++++++++++++++++++++++++++++++++--
 src/local.mk        |  1 +
 src/splice.h        | 41 +++++++++++++++++++
 src/yes.c           | 15 +------
 tests/cat/splice.sh | 52 ++++++++++++++++++++++++
 tests/local.mk      |  1 +
 7 files changed, 196 insertions(+), 16 deletions(-)
 create mode 100644 src/splice.h
 create mode 100755 tests/cat/splice.sh

diff --git a/NEWS b/NEWS
index 2fabd07b7..f03f13d91 100644
--- a/NEWS
+++ b/NEWS
@@ -39,6 +39,10 @@ GNU coreutils NEWS                                    -*- 
outline -*-
 
 ** Improvements
 
+  'cat' now uses zero-copy I/O on Linux when the input or output are not 
regular
+  files to significantly increase throughput.
+  E.g., throughput improved 4x from 1.8GiB/s to 7.9GiB/s on a Power10 system.
+
   'df --local' recognises more file system types as remote.
   Specifically: autofs, ncpfs, smb, smb2, gfs, gfs2, userlandfs.
 
diff --git a/src/cat.c b/src/cat.c
index f9c92005c..2c63d803e 100644
--- a/src/cat.c
+++ b/src/cat.c
@@ -37,6 +37,9 @@
 #include "ioblksize.h"
 #include "fadvise.h"
 #include "full-write.h"
+#include "isapipe.h"
+#include "splice.h"
+#include "unistd--.h"
 #include "xbinary-io.h"
 
 /* The official name of this program (e.g., no 'g' prefix).  */
@@ -545,6 +548,86 @@ copy_cat (void)
       }
 }
 
+/* Copy data from input to output using splice if possible.
+   Return 1 if successful, 0 if ordinary read+write should be tried,
+   -1 if a serious problem has been diagnosed.  */
+
+static int
+splice_cat (void)
+{
+  bool some_copied = false;
+  bool ok = true;
+
+#if HAVE_SPLICE
+
+  bool input_is_pipe = 0 < isapipe (input_desc);
+  bool stdout_is_pipe = 0 < isapipe (STDOUT_FILENO);
+
+  idx_t pipe_size = 0;
+  if (input_is_pipe)
+    pipe_size = increase_pipe_size (input_desc);
+  if (stdout_is_pipe)
+    pipe_size = MAX (pipe_size, increase_pipe_size (STDOUT_FILENO));
+
+  int pipefd[2] = { -1, -1 };
+  int outfd;
+
+  /* Avoid creating an intermediate pipe if possible.  */
+  if (input_is_pipe || stdout_is_pipe)
+    outfd = STDOUT_FILENO;
+  else
+    {
+      if (pipe (pipefd) < 0)
+        return false;
+      outfd = pipefd[1];
+      pipe_size = increase_pipe_size (pipefd[0]);
+    }
+
+  while (true)
+    {
+      ssize_t bytes_read = splice (input_desc, NULL, outfd, NULL,
+                                   pipe_size, 0);
+      if (bytes_read <= 0)
+        goto done;
+      if (outfd == STDOUT_FILENO)
+        some_copied = true;
+      else
+        {
+          /* We need to drain the intermidate pipe to standard output.  */
+          while (0 < bytes_read)
+            {
+              ssize_t bytes_written = splice (pipefd[0], NULL, STDOUT_FILENO,
+                                              NULL, pipe_size, 0);
+              /* If we have not written output using splice but the offset of
+                 INPUT_DESC has increased from the first splice call, we must
+                 restore the original offset before falling back to read and
+                 write.  */
+              if (bytes_written < 0 && ! some_copied
+                  && lseek (input_desc, -bytes_read, SEEK_CUR) < 0)
+                {
+                  error (0, errno, "%s", quotef (infile));
+                  ok = false;
+                }
+              if (bytes_written <= 0)
+                goto done;
+              some_copied = true;
+              bytes_read -= bytes_written;
+            }
+        }
+    }
+
+ done:
+  if (0 <= pipefd[0])
+    {
+      int saved_errno = errno;
+      close (pipefd[0]);
+      close (pipefd[1]);
+      errno = saved_errno;
+    }
+#endif
+
+  return ok ? some_copied : -1;
+}
 
 int
 main (int argc, char **argv)
@@ -760,9 +843,18 @@ main (int argc, char **argv)
             }
           else
             {
-              insize = MAX (insize, outsize);
-              inbuf = xalignalloc (page_size, insize);
-              ok &= simple_cat (inbuf, insize);
+              int splice_cat_status = splice_cat ();
+              if (splice_cat_status != 0)
+                {
+                  inbuf = NULL;
+                  ok &= 0 < splice_cat_status;
+                }
+              else
+                {
+                  insize = MAX (insize, outsize);
+                  inbuf = xalignalloc (page_size, insize);
+                  ok &= simple_cat (inbuf, insize);
+                }
             }
         }
       else
diff --git a/src/local.mk b/src/local.mk
index bf88f7d0e..9d9c9814b 100644
--- a/src/local.mk
+++ b/src/local.mk
@@ -61,6 +61,7 @@ noinst_HEADERS =              \
   src/remove.h                 \
   src/set-fields.h             \
   src/show-date.h              \
+  src/splice.h                 \
   src/statx.h                  \
   src/system.h                 \
   src/temp-stream.h            \
diff --git a/src/splice.h b/src/splice.h
new file mode 100644
index 000000000..1fb55054d
--- /dev/null
+++ b/src/splice.h
@@ -0,0 +1,41 @@
+/* Common definitions for splice and vmsplice.
+   Copyright (C) 2026 Free Software Foundation, Inc.
+
+   This program is free software: you can redistribute it and/or modify
+   it under the terms of the GNU General Public License as published by
+   the Free Software Foundation, either version 3 of the License, or
+   (at your option) any later version.
+
+   This program is distributed in the hope that it will be useful,
+   but WITHOUT ANY WARRANTY; without even the implied warranty of
+   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+   GNU General Public License for more details.
+
+   You should have received a copy of the GNU General Public License
+   along with this program.  If not, see <https://www.gnu.org/licenses/>.  */
+
+#ifndef SPLICE_H
+# define SPLICE_H 1
+
+# if HAVE_SPLICE
+
+/* Empirically determined pipe size for best throughput.
+   Needs to be <= /proc/sys/fs/pipe-max-size  */
+enum { SPLICE_PIPE_SIZE = 512 * 1024 };
+
+static inline idx_t
+increase_pipe_size (int fd)
+{
+  int pipe_cap = 0;
+#  if defined F_SETPIPE_SZ && defined F_GETPIPE_SZ
+  if ((pipe_cap = fcntl (fd, F_SETPIPE_SZ, SPLICE_PIPE_SIZE)) < 0)
+    pipe_cap = fcntl (fd, F_GETPIPE_SZ);
+#  endif
+  if (pipe_cap <= 0)
+    pipe_cap = 64 * 1024;
+  return pipe_cap;
+}
+
+# endif
+
+#endif
diff --git a/src/yes.c b/src/yes.c
index 1a1d74ce5..d111b125e 100644
--- a/src/yes.c
+++ b/src/yes.c
@@ -27,6 +27,7 @@
 #include "full-write.h"
 #include "isapipe.h"
 #include "long-options.h"
+#include "splice.h"
 #include "unistd--.h"
 
 /* The official name of this program (e.g., no 'g' prefix).  */
@@ -76,10 +77,6 @@ repeat_pattern (char *dest, char const *src, idx_t srcsize, 
idx_t bufsize)
 
 #if HAVE_SPLICE
 
-/* Empirically determined pipe size for best throughput.
-   Needs to be <= /proc/sys/fs/pipe-max-size  */
-enum { SPLICE_PIPE_SIZE = 512 * 1024 };
-
 /* Enlarge a pipe towards SPLICE_PIPE_SIZE and return the actual
    capacity as a quarter of the pipe size (the empirical sweet spot
    for vmsplice throughput), rounded down to a multiple of COPYSIZE.
@@ -88,15 +85,7 @@ enum { SPLICE_PIPE_SIZE = 512 * 1024 };
 static idx_t
 pipe_splice_size (int fd, idx_t copysize)
 {
-  int pipe_cap = 0;
-# if defined F_SETPIPE_SZ && defined F_GETPIPE_SZ
-  if ((pipe_cap = fcntl (fd, F_SETPIPE_SZ, SPLICE_PIPE_SIZE)) < 0)
-    pipe_cap = fcntl (fd, F_GETPIPE_SZ);
-# endif
-  if (pipe_cap <= 0)
-    pipe_cap = 64 * 1024;
-
-  size_t buf_cap = pipe_cap / 4;
+  size_t buf_cap = increase_pipe_size (fd) / 4;
   return buf_cap / copysize * copysize;
 }
 
diff --git a/tests/cat/splice.sh b/tests/cat/splice.sh
new file mode 100755
index 000000000..ab2a105b7
--- /dev/null
+++ b/tests/cat/splice.sh
@@ -0,0 +1,52 @@
+#!/bin/sh
+# Test some cases where 'cat' uses the splice system call.
+
+# Copyright (C) 2026 Free Software Foundation, Inc.
+
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <https://www.gnu.org/licenses/>.
+
+. "${srcdir=.}/tests/init.sh"; path_prepend_ ./src
+print_ver_ cat
+uses_strace_
+
+# Check the non pipe output case, since that is different with splice
+if timeout 10 true; then
+  timeout .1 cat /dev/zero >/dev/null
+  test $? = 124 || fail=1
+fi
+
+# Ensure we fallback to write() if there is an issue with (async) zero-copy
+zc_syscalls='io_uring_setup io_uring_enter io_uring_register memfd_create
+             sendfile splice tee vmsplice'
+syscalls=$(
+  for s in $zc_syscalls; do
+    strace -qe "$s" true >/dev/null 2>&1 && echo "$s"
+  done | paste -s -d,)
+
+no_zero_copy() {
+  strace -f -o /dev/null -e inject=${syscalls}:error=ENOSYS "$@"
+}
+if no_zero_copy true; then
+  test "$(no_zero_copy cat /dev/zero | head -c 2 | tr '\0' 'y')" = 'yy' \
+    || fail=1
+fi
+# Ensure we fallback to write() if there is an issue with pipe2()
+# For example if we don't have enough file descriptors available.
+no_pipe() { strace -f -o /dev/null -e inject=pipe,pipe2:error=EMFILE "$@"; }
+if no_pipe true; then
+  no_pipe timeout .1 cat /dev/zero >/dev/null
+  test $? = 124 || fail=1
+fi
+
+Exit $fail
diff --git a/tests/local.mk b/tests/local.mk
index 590978297..2e889e207 100644
--- a/tests/local.mk
+++ b/tests/local.mk
@@ -308,6 +308,7 @@ all_tests =                                 \
   tests/cat/cat-proc.sh                                \
   tests/cat/cat-buf.sh                         \
   tests/cat/cat-self.sh                                \
+  tests/cat/splice.sh                          \
   tests/misc/basename.pl                       \
   tests/basenc/base64.pl                       \
   tests/basenc/basenc.pl                       \
-- 
2.53.0


Reply via email to