The fastest rm command and one of the fastest cp commands
alexsaveau.dev
alexsaveau.dev
$ time rm -rf Xcode.app
real 0m39.850s
user 0m0.429s
sys 0m29.153s
$ time rmz Xcode.app
real 0m36.476s
user 0m1.468s
sys 1m59.916s
It's a little bit faster, but not by much. Despite claims that it runs in parallel, it seems like it really just hits unlink on many more cores and contends on the kernel's filesystem spinlocks rather than doing useful work. So the end result is that rm uses 70% CPU and rmz uses 400% CPU and they basically end up doing the same thing.(FWIW, I don't use rm when deleting Xcode anyways, because it takes long and I do it too often. When testing unxip I just have it write to a temporary APFS volume and wipe it in between runs, which takes all of 10 seconds.)
$ du -Ash Xcode.app
22G Xcode.app
> $ du -Ash Xcode.app
> 22G Xcode.app
My installation of Xcode only takes up 3.6G and is still fully operational.
As a developer of both desktop and mobile apps, I discovered some time ago that I could delete most, if not all, of the Xcode simulators that I don’t use without affecting the functionality of the IDE. The software is smart enough to download additional assets when you attempt to launch one of the simulators. I believe that, by default, Xcode comes with simulators for the latest versions of iOS, iPad OS, Watch OS, and Apple TV, sometimes including two or three versions for each platform.
I think this is the reason why your installation weights 22G.
Not the reason OP is deleting Xcode, but you'd be surprised at how buggy a lot of stuff involving Xcode can be.
Therefore I update Xcode by downloading the .xip from developer.apple.com/downloads.
[1] https://github.com/SUPERCILEX/fuc/tree/master/comparisons
I would guess that macOS, being targeted at desktops and the typical workloads being run on them, hasn't received the same level of scrutiny in this particular area.
"The macOS/Windows implementations are currently equivalent to the *_rayon implementations shown in the benchmarks."
Rayon is pretty good, but clearly suboptimal as evidenced by the benchmarks.
Once the file is in the recycle bin, it will probably be months before final deletion happens, and windows will not ask before doing so.
To rephrase:
> mv dir .old-dir && rm -r .old-dir &
it's how most GUI desktops “delete” stuff, they just run the two actions separately
It's kind of a stretch anyway since the core idea of the command was the final `&`
Hmm, the best I can find, no OS does this by default although all of them are capable of doing it.
This reminds me of a similar trick to speed up deallocations by moving them to a new thread: https://abramov.io/rust-dropping-things-in-another-thread
Always something interesting to learn at the margins
Now the second interview question: how did trickle-rm work? How can you simply "reflect" the back pressure of the "x rm's per second" constraint back onto the "walk the directory tree to find the rm's to do" so that the tree walk generates a trickle of i/o operations?
I'm surprised that rm's io would cause that much of an issue. I thought it only removed entries from the partition table.
Actually I'm on btrfs so reflink copy is like instant I think? I should test that better.
That, or your algorithm could be optimal for concurrency already - and you would see an immediate performance improvement.
With blocking I/O and parallelism you have a thread ready to go when the operation is complete. You have N threads for N iops. With concurrency you have to dequeue completed work, and then delegate that work to (usually) fewer than N threads. Dequeuing completed iops takes time (it's an extra syscall), and there may not be a thread ready hand the completed iop. More latency.
Running 1000s of threads isn't realistic because your OS would typically grind to a halt, so concurrency is unavoidable. It does have a cost, though.
To be clear, the added latency here is better than the work never happening at all (which would be the result of running 1000s of threads on modern mainstream operating systems), but there is unavoidable latency if you are handling >N iops with N threads (which is intrinsic to the definition of concurrency).
I am referring to the broad, general case, much like big-O works. You can find numerous exception to big-O, such as preferring arrays over hashes when the set is very small. Let's invent big-L notation, N is the number of threads, M is number of iops. With pure parallelism you have L(N), with pure concurrency you have L(M), and with a hybrid you have L(M-N).
The whole point of io_uring is to drastically decrease the number of sys calls, creating channels where more requests can be filed with lower than traditional cost of a readFile syscall for example, and where completion can also be lower overhead delivery of events.
So historically I kind of would have agreed with the parent. Today, we don't really know! Hence my excitement.
In theory. I did some work on high performance filesystem I/O on Linux about a year ago, doing intensive random-access to fast SSDs, and found io_uring to be slightly slower than a well-tuned thread pool with an appropriate queue depth.
That was a little surprising as the thread pool has to do system calls for each I/O operation and io_uring does not. Perhaps it is faster with newer kernels or other access patterns.
io_uring is better able to adapt autonatically to different numbers of cores, device queue depth and amount of filesystem data cache residency. That comes from it having access to kernel state which is not made available to userspace on Linux, to guide thread offloading decisions, rather than from the ringbuffer communication.
not to fanboy out TOO much, but your posting on HyBi was & is greatly influential to me. see, https://github.com/rektide/pipe-layer#essence
(sorely neglected project to me but also still very near & dear, still a core principle & value in my pantheon of beliefs)]
no particular comment on io_uring. thankfully jens keeps making it better. the numbers he posts for his synthetics keep seeming impossibly good. but i fully am ready to believe the real situation is more complicated.
i do wish we'd see some uptake from the usual suspects. both Deno and Node have delayed/deferred work on these topics. but supposedly slowly happening in node. https://github.com/libuv/libuv/issues/1947 https://github.com/denoland/deno/issues/16232
i'm clearly missing something here
parallel execution helps when operations are cpu bound
file operations are (almost always) io bound
and totally unclear how directories represent an "interference" boundary
bizarre
The question is, how independent are IO operations in separate directories. And the article is claiming that they're fairly independent and don't block each other.
maybe this is what you mean by independent?
but the thing is that in disk io, directory structure is (as far as i know) basically unrelated to relevant contentious resources, when measuring speed
maybe if you're doing a billion small files than overhead begins to matter, but copying 3 big files from 3 different directories is gonna take just as long if you do them in parallel vs. if you do them sequentially
that may not be true if they're on different disks, but that kind of proves my point, the directory isn't the factor, the underlying disk is
> The question is, how independent are IO operations in separate directories. And the article is claiming that they're fairly independent and don't block each other.
yeah and in this sense the article is misleading, because (as far as i know) directories are basically unrelated to independence in the general case
but this is all a bit tangential as the article is about file system stuff
Plus accompanying benchmark: https://alexsaveau.dev/blog/projects/performance/files/fuc/f...
---
> file operations are (almost always) io bound
This is a common misconception. It was presumably true a decade ago, but PCIe is getting exponentially faster every 3 years: https://arstechnica.com/gadgets/2022/06/months-after-finaliz...
The NVMe protocol has extremely deep queues [1] and leaving them empty means leaving performance on the table. I think you'll find it surprisingly difficult to saturate the PCIe bus with just one core: PCIe 7 will support 512GB/s. Assuming a single core can produce 64 bytes (an entire cache line!) per cycle running at 5GHz, you're still only at 320/512=62.5% saturation. This napkin math is a little BS, but my point is that individual cores are quickly going to be outpaced by bandwidth availability.
> and totally unclear how directories represent an "interference" boundary
To add a bit more color here, it depends on how your file system is implemented. I belive Windows stores every file's metadata in a global database, so segmenting operations by directory yields no benefits. On the other hand, Unix FSs tend to store file_name to inode mappings per directory, so creating a new mapping in one directory doesn't interfere with another directory.
[1]: https://en.wikipedia.org/wiki/NVM_Express#Comparison_with_AH...
is disk IO bottlenecked by NVMe/PCIe limits, or by disk iops limits?
> Unix FSs tend to store file_name to inode mappings per directory, so creating a new mapping in one directory doesn't interfere with another directory.
again, you're handwaving on what "interfere" means
do "inode mappings" represent resources with no shared resource constraints?
is reading one "inode mapping" as fast as you can with one core independent from reading a different "inode mapping" as fast as you can with a separate core?
afaik it is not, am i wrong?
Note that I'm out of my depth here, so this is all speculation. Until we hit hardware limitations (which will be PCIe 6 if I had to guess), I'm pretty sure those are the same thing. One read/write iop = 4KiB. If your PCIe bandwidth is limited to N GB/s, then there are only so many iops you can physically send/receive to/from the SSD, regardless of how many iops the SSD could be capable of processing. So currently we're bottlenecked by PCIe, but I doubt that will continue to be the case.
> again, you're handwaving on what "interfere" means
It depends on how the file system is implemented, but my guess would be that a lock on the inode or block cache entry is acquired.
> do "inode mappings" represent resources with no shared resource constraints?
Those are the contents of the directory. The problem is not reading them, but changing them.
that cache coalesces and serializes access to disk, it does all of this "locking" you're referring to, and it's very smart
it seems like you're writing code assuming this intermediating layer does not exist?
why do you think this is true?
i've never heard of anything like it
directories are inodes on a file system, they are in no way "shared resources for their direct children", and there is no concept of a "directory-modifying operation" which contends with operations on any file (or directory) which is a "child" (subdir, sub-file) of that directory
your claim is totally bizarre to me, afaik it's nonsensical, but i guess i could be mistaken
Also no need to theorize: run the benchmark I linked for yourself. It clearly shows a massive advantage to having each thread work with its own directory.
2. the overhead of modifying the dirent is statistically zero compared to the costs related to manipulating the files on disk
$ hyperfine --warmup 3 -N "./test /dev/shm 8 zip" "./test /dev/shm 8 chain" Benchmark 1: ./test /dev/shm 8 zip Time (mean ± σ): 118.5 ms ± 11.6 ms [User: 92.9 ms, System: 726.6 ms] Range (min … max): 103.6 ms … 143.4 ms 23 runs
Benchmark 2: ./test /dev/shm 8 chain Time (mean ± σ): 235.7 ms ± 11.0 ms [User: 116.4 ms, System: 1537.7 ms] Range (min … max): 220.1 ms … 258.3 ms 13 runs
Summary './test /dev/shm 8 zip' ran 1.99 ± 0.22 times faster than './test /dev/shm 8 chain'
i mean ignore me if you want, no skin off my back
but you're not benchmarking what you think you're benchmarking
this synchronization is handled for you by the fs, specifically the fs cache
inode alignment and errors are managed by this intermediating layer
your benchmarks are not demonstrating what you think they are demonstrating
the fs cache does most/all of the optimizations you're doing manually
bypassing the fs cache is highly atypical for user-space code
https://github.com/SUPERCILEX/fuc/tree/master/comparisons#re...