<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Performance on KDE Blogs</title><link>https://blogs.kde.org/tags/performance/</link><description>Recent content in Performance on KDE Blogs</description><generator>Hugo -- gohugo.io</generator><language>en</language><lastBuildDate>Mon, 27 Jul 2026 09:22:42 +0000</lastBuildDate><atom:link href="https://blogs.kde.org/tags/performance/index.xml" rel="self" type="application/rss+xml"/><item><title>Making KIO copy many files fast</title><link>https://blogs.kde.org/2026/07/28/making-kio-copy-many-files-fast/</link><pubDate>Tue, 28 Jul 2026 00:00:01 +0000</pubDate><author>Méven Car</author><guid>https://blogs.kde.org/2026/07/28/making-kio-copy-many-files-fast/</guid><description>&lt;p&gt;There is a bug in KDE's bug tracker that is almost as old as some of our
contributors: &lt;a href="https://bugs.kde.org/show_bug.cgi?id=342056"&gt;bug 342056&lt;/a&gt;,
&amp;quot;Ridiculously slow file copy (multiple small files)&amp;quot;, reported by Alexander
Nestorov back in 2014. The report is blunt: copying a 15 GB folder of roughly 3
million small files took 5 to 10 hours in KDE, versus about 20 minutes with
&lt;code&gt;rsync&lt;/code&gt;. One commenter summed up the mood:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;I generally use cp and rsync instead of dolphin for anything more than a few files.&lt;/p&gt;&lt;/blockquote&gt;
&lt;p&gt;That bug has been stuck in my head for a while, so this post is about finally
fixing it. I have been working on KIO's copy path, and I want to walk
through where the time was going, some history that explains why, and show
numbers across KIO versions including a plain &lt;code&gt;cp&lt;/code&gt; for reference.&lt;/p&gt;
&lt;p&gt;At with any optimization, let's measure first:&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blogs.kde.org/2026/07/28/making-kio-copy-many-files-fast/offcpu-socket.svg"&gt;&lt;figure&gt;
 &lt;img class="img-fluid" alt="Off-CPU flame graph of a KIO 6.28 copy of 5000 small files, with the socket round-trip frames highlighted" src="https://blogs.kde.org/2026/07/28/making-kio-copy-many-files-fast/offcpu-socket.svg"
 style="max-width: 100%; height: auto"
 /&gt;
&lt;/figure&gt;
&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Where a KIO 6.28 copy of 5000 small files spends its blocking time, and there
is no single hotspot. Each file blocks in a burst of short syscalls: reading the
mount table (&lt;code&gt;/proc/self/mountinfo&lt;/code&gt;), opening the source and destination, and
round-tripping a command to the worker over an internal socket. The socket
round-trips, the send and receive plus the waits for each reply, are the frames
highlighted in red, about 15% of the blocking here, spread as thin slivers
across both threads because every one of the thousands of files pays them. The
frames in green are the real filesystem work the copy cannot avoid: the &lt;code&gt;statx&lt;/code&gt;
and &lt;code&gt;openat&lt;/code&gt; calls, &lt;code&gt;copy_file_range&lt;/code&gt; moving the bytes, and the &lt;code&gt;ext4&lt;/code&gt; metadata
updates beneath them. That per-file storm, not any single call, is what the rest
of this post discusses.
(Off-CPU flame graph from &lt;code&gt;sched:sched_switch&lt;/code&gt;, weighted by number of blocking
switches; click to open the full SVG.)&lt;/em&gt;&lt;/p&gt;
&lt;h2 id="a-little-history"&gt;A little history&lt;/h2&gt;
&lt;p&gt;KIO is the layer behind almost every file operation in KDE software, from Dolphin's copy
and paste to the network transparency that lets you open &lt;code&gt;sftp://&lt;/code&gt; or &lt;code&gt;smb://&lt;/code&gt;
URLs as if they were local. Its design goes back to the KDE 2 days, around the
year 2000. Back then the answer to &amp;quot;how do I do I/O without freezing the UI&amp;quot; was
not a thread but a separate process: a &lt;em&gt;worker&lt;/em&gt; (we used to call them kioslaves)
is launched for a protocol and talks to the application over a socket. This
predates usable threading on Linux (&lt;a href="https://en.wikipedia.org/wiki/Native_POSIX_Thread_Library"&gt;Native POSIX Thread
Library&lt;/a&gt; only landed
in Linux 2.6 in 2003), so processes plus socket IPC was the portable, robust
choice, and it still is for network protocols today: if an &lt;code&gt;smb://&lt;/code&gt; worker
crashes, your file manager does not go down with it.&lt;/p&gt;
&lt;p&gt;The flip side is cost. Every request and its reply are serialized and sent over
that socket, with an event-loop hop on each side. For &lt;code&gt;file://&lt;/code&gt; that overhead
buys you nothing, because there is no untrusted network on the other end, just
the local disk.&lt;/p&gt;
&lt;p&gt;In 2022 David Faure changed that: &lt;a href="https://invent.kde.org/frameworks/kio/-/merge_requests/740"&gt; Implement running KIO workers in-process
using a thread &lt;/a&gt;
landed in Frameworks 5.95. Since then the &lt;code&gt;file&lt;/code&gt; worker runs on a thread inside
the application instead of as a separate process (other protocols stay
out-of-process for robustness). That removed process launch and context-switch
cost.&lt;/p&gt;
&lt;h2 id="no-socket-between-the-thread-and-the-app-629"&gt;No socket between the thread and the app (6.29)&lt;/h2&gt;
&lt;p&gt;There was still a catch that had been hiding in plain sight: even in-process,
the worker thread and the application talked to each other over a socketpair,
serializing every command as if they were separate processes. Once the thread
was used, that socket was pure overhead.&lt;/p&gt;
&lt;p&gt;That's the red highlight in the first flamegraph.&lt;/p&gt;
&lt;p&gt;So in-process workers now use a real in-memory transport instead of a loopback
socket. A new &lt;code&gt;ThreadConnectionBackend&lt;/code&gt; handles commands, and for reads the actual
data buffer, straight to the peer thread, with no serialization and no syscall,
which makes in-process file reads zero-copy. This is the biggest single jump in
the numbers below, and it has landed on master for 6.29
(&lt;a href="https://invent.kde.org/frameworks/kio/-/merge_requests/2279"&gt;kio!2279&lt;/a&gt;).&lt;/p&gt;
&lt;h2 id="next-batching-the-copy-and-the-stat-under-review"&gt;Next: batching the copy and the stat (under review)&lt;/h2&gt;
&lt;p&gt;The changes above make each command cheap. The remaining cost is that there are
still so many of them: copying N files is N separate &lt;code&gt;file_copy&lt;/code&gt; jobs, each one
going through the job scheduler and taking its own round-trip to the worker. For
many small files that fixed per-file cost, not the data, is what dominates.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;CopyJob&lt;/code&gt; can dispatch a whole batch of plain local-to-local files to the worker
in one command, and the worker copies them (using &lt;code&gt;copy_file_range&lt;/code&gt;,
and reflink on filesystems like Btrfs or XFS that support it). Each file still
gets its progress signal, its byte accounting, its undo entry, and anything
the worker cannot do blindly — an existing destination, an unreadable source, a
rename or skip decision — is handed back to the normal per-file path. The gate is
conservative on purpose so a hung mount can never freeze the batch
(&lt;a href="https://invent.kde.org/frameworks/kio/-/merge_requests/2282"&gt;kio!2282&lt;/a&gt;).&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blogs.kde.org/2026/07/28/making-kio-copy-many-files-fast/offcpu-batch.svg"&gt;&lt;figure&gt;
 &lt;img class="img-fluid" alt="Off-CPU flame graph after batch-copy, with the socket round-trip frames gone" src="https://blogs.kde.org/2026/07/28/making-kio-copy-many-files-fast/offcpu-batch.svg"
 style="max-width: 100%; height: auto"
 /&gt;
&lt;/figure&gt;
&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;The same copy on 6.29 with batch-copy: the red socket round-trips are gone. Two
things removed them: the in-memory transport from the previous section (no
socketpair at all) and batch-copy folding a whole run of files into one command
instead of one round-trip each. What is left is real filesystem work: the green
frames are the copy itself, &lt;code&gt;copy_file_range&lt;/code&gt; moving the bytes, the &lt;code&gt;openat&lt;/code&gt; and
&lt;code&gt;statx&lt;/code&gt; calls, and the &lt;code&gt;ext4&lt;/code&gt; metadata beneath them. The wide plateau that is not
green is the benchmark loop deleting each pass's files before the next run
(&lt;code&gt;unlink&lt;/code&gt;), which is teardown rather than part of the copy.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Before copying, &lt;code&gt;CopyJob&lt;/code&gt; stats every source, which is another per-file
round-trip. The follow-up batches the stat phase over the same in-process lane.
This is the &lt;code&gt;batch-copy-stat&lt;/code&gt; column below.&lt;/p&gt;
&lt;p&gt;Both of these are under review and targeted at a release after 6.29, so treat
their columns in the table as a preview of what is coming rather than something
you can install today.&lt;/p&gt;
&lt;h2 id="numbers"&gt;Numbers&lt;/h2&gt;
&lt;p&gt;Methodology: a 13th-gen Intel Core i7-1365U laptop, everything built
&lt;code&gt;RelWithDebInfo&lt;/code&gt; without any sanitizer, copying onto a real ext4 volume (so no
reflink shortcut, &lt;code&gt;copy_file_range&lt;/code&gt; moves real bytes). Each KIO version supplies
both its own &lt;code&gt;libKIOCore&lt;/code&gt; and its own &lt;code&gt;kio_file&lt;/code&gt; worker. Times are the median of
5 runs (3 for the large set) after a discarded warm-up, so caches are warm and
equal for every version. &lt;code&gt;cp -r --preserve=mode,timestamps&lt;/code&gt; is included as the
floor, since that is what the bug reporter reached for instead of Dolphin.&lt;/p&gt;
&lt;p&gt;5000 files of 4 KB each (the many-small-files case from the bug):&lt;/p&gt;
&lt;table&gt;
 &lt;thead&gt;
 &lt;tr&gt;
 &lt;th&gt;Version&lt;/th&gt;
 &lt;th&gt;Median time&lt;/th&gt;
 &lt;/tr&gt;
 &lt;/thead&gt;
 &lt;tbody&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;cp -r&lt;/code&gt; (coreutils)&lt;/td&gt;
 &lt;td&gt;81 ms&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;KIO 6.28.0&lt;/td&gt;
 &lt;td&gt;1616 ms&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;KIO 6.29-dev (master)&lt;/td&gt;
 &lt;td&gt;396 ms&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;KIO 6.29-dev + batch-copy&lt;/td&gt;
 &lt;td&gt;88 ms&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;KIO 6.29-dev + batch-copy-stat&lt;/td&gt;
 &lt;td&gt;93 ms&lt;/td&gt;
 &lt;/tr&gt;
 &lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;1000 files of 5 MB each (bulk data, where the copy is I/O-bound):&lt;/p&gt;
&lt;table&gt;
 &lt;thead&gt;
 &lt;tr&gt;
 &lt;th&gt;Version&lt;/th&gt;
 &lt;th&gt;Median time&lt;/th&gt;
 &lt;/tr&gt;
 &lt;/thead&gt;
 &lt;tbody&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;cp -r&lt;/code&gt; (coreutils)&lt;/td&gt;
 &lt;td&gt;1312 ms&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;KIO 6.28.0&lt;/td&gt;
 &lt;td&gt;1997 ms&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;KIO 6.29-dev (master)&lt;/td&gt;
 &lt;td&gt;1687 ms&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;KIO 6.29-dev + batch-copy&lt;/td&gt;
 &lt;td&gt;1506 ms&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;KIO 6.29-dev + batch-copy-stat&lt;/td&gt;
 &lt;td&gt;1454 ms&lt;/td&gt;
 &lt;/tr&gt;
 &lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;The small-files case is the story. KIO 6.28 was about 20x slower than &lt;code&gt;cp&lt;/code&gt;,
which is exactly the ratio the 2014 report complained about. Removing the socket
for in-process workers, together with no longer probing the destination
filesystem once per file, takes it from about 1.6 s to 0.4 s (roughly 4x).
Batching the copy takes it to 88 ms, essentially &lt;code&gt;cp&lt;/code&gt; speed and about 18x faster
than 6.28. That &amp;quot;I use cp instead of dolphin&amp;quot; line finally has an answer.&lt;/p&gt;
&lt;p&gt;The large-file case was already well covered before any of this work, and the
table shows it: the copy is bound by moving the bytes, so even KIO 6.28 stayed
close to &lt;code&gt;cp&lt;/code&gt; there, and the per-file optimizations barely move the number
(batch-copy lands within about 15% of &lt;code&gt;cp&lt;/code&gt;). That is the whole reason this
effort targets many small files instead, where the fixed per-file cost, not the
data, is what dominates.&lt;/p&gt;
&lt;p&gt;batch-copy and batch-copy-stat come out tied here: the benchmark copies a single
directory, so its entries arrive in one listing and batch-stat has nothing to
batch. It helps the other case, copying a large selection of individual files
that would otherwise be one stat round-trip each. Since it removes the
round-trip and not the &lt;code&gt;stat&lt;/code&gt; call, the win is largest when stats are cheap, not
when the disk is slow. Read the two as equal on this test.&lt;/p&gt;
&lt;p&gt;And &lt;code&gt;cp&lt;/code&gt; will almost certainly stay a little ahead, by design: KIO does more
than copy the bytes. It reports per-file progress, can be paused and cancelled,
keeps an undo record, preserves permissions and timestamps, resolves conflicts,
and tells any open file manager what changed so its view refreshes. That work is
not free, and the goal was never to beat &lt;code&gt;cp&lt;/code&gt;, only to make the local case fast
enough that reaching for &lt;code&gt;cp&lt;/code&gt; instead of Dolphin stops being the obvious choice.&lt;/p&gt;
&lt;h2 id="caveats-and-what-is-next"&gt;Caveats and what is next&lt;/h2&gt;
&lt;p&gt;These are laptop numbers on one ext4 disk with warm caches, meant to compare
versions, not to be absolute. On Btrfs or XFS the batch path reflinks instead of
copying, which changes the large-file picture entirely. The batch fast path only
engages for plain local-to-local copies on a responsive filesystem; anything
else (network, FAT/NTFS, conflicts needing a dialog) falls back to the existing
per-file path, unchanged.&lt;/p&gt;
&lt;p&gt;Thanks to David Faure for the in-process worker threads that made all of this
possible, to Frank Reininghaus for the earlier listing fixes, and to Alexander
Nestorov for a bug report that aged well enough to still be worth closing.&lt;/p&gt;
&lt;p&gt;That's all, folks.&lt;/p&gt;</description></item></channel></rss>