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