Ordering Requests to Accelerate Disk I/O
pkolaczk.github.io
pkolaczk.github.io
* also do it when walking directories! on ext4 you can FIEMAP the directories while walking them.
* keep a readahead buffer that spans multiple files, that way you can keep X megabytes in flight even when the files are small
* do readaheads on directories (can only be done as root for ext4)
* do drop-behind to prevent page cache thrashing
* even when writing synchronous code you can use io_uring to batch syscalls such as statx(), open() and fadvise()
I wrote crates for some of those points: https://crates.io/crates/platter-walk https://crates.io/crates/reapfrog
There is a general readahead in Linux though, see eg blkdev(8).
For many use cases the small randomish iops rate is the bottleneck (think database index), and because of the small latencies in flash storage you often want to keep the readahead modest absent some detected pattern of sequential io.
https://www.reddit.com/r/rust/comments/mk2nz8/ordering_reque...
"NCQ does it's best to order all of the outstanding commands at a moment in time (so maybe 32). This is instead submitting the commands in the optimal order to start with."
The thread contains more details.
We eventually found that we could do a whole bunch of random I/O to a disk and from the timing infer the underlying disk geometry, the rotation speed, and a reasonable model of seek speed. From that we could predict the fastest order for a given sequence of I/Os.
This was a decent performance enhancement, but the step of characterizing the disk took a long time (at least a day or two if I recall correctly), and we decided that consumers would not put up with that and so never turned this into a product.
Another thing we played around with was monitoring for files that were consecutive on the disk (and thanks to defraggers that was common) and that tended to be read consecutively, but had places where the reader consistently spent enough time between reads that they would blow a rev on the disk. We would then purposefully refragment that file at those spots, arranging the fragments so that when the reader was ready to issue the next read the disk was in the right position.
We decided that would probably be a viable product, but thought we could do better so didn't pursue it much past that. We did later do a Mac degfragger, but without the purposeful fragging of some file, so the time spent on defraggers turned out not to be wasted.
The next approach built on the approach of monitoring I/O and looking for things that happened repeatedly and finding ways to optimize those. What we hit on was caching during program launch. This was a time when big GUI apps like browsers, word processors, Photoshop, etc. took 10 seconds or more to start. If we could shave a few seconds off of that it would be very noticeable.
When things like that launch, they open and read from many files (the main program file, dynamic libraries it uses, config files, fonts, template files, and who knows what else). For many of these files they are opened and partly read (a header, say), and then other things are read and processed, and then it comes back to that earlier file and reads some more.
If you have something that recognizes when a program is launching, looks to see if it is something that it has seen a few times before and that has a lot of the same I/O patterns each launch, and if it does looks at the size of the system's read cache, it can use its knowledge of what I/O will be happening in what order over the next several seconds to preload the cache. For instance, if it knows that the program is going to read a 1 block header on file A, then several seconds latter is going to read another 5 blocks after that header, it can preload the cache with all 6 blocks using a single read of A.
This turns out to work quite well. We were knocking something 50% off the launch time off many of the bug GUI programs, sometimes even more. This did become a product and did reasonably well, although it had a stupid name (Superfassst!) [1]. Someone slipped a copy to Jerry Pournelle at Comdex as he went to the meeting where BYTE would decide their Best of Comdex awards, he tried it during the meeting, and it almost became Best Utility. (Later, though, he hit some stability problems, so couldn't fully recommend it). (Search for "superfasst" in this [2] if you want to see his remarks).
A few US distributors wanted to distribute it. One was a relatively small, relatively new and unknown distributor who thought our product could be the thing that establishes them. The best deal, though, was offered by one whose previous big product had been one of those RAM doublers. That turned out to be a fraud (VxD that was basically just the sample from the DDK, and an interface that lied). That troubled us, and the explanation they offered is that the developer had lied to them.
We would not have believed them...except our CEO/Owner knew that developer, it turned out. The develop had been his co-founder at his previous company. In that company, our owner was in charge of producing the product, which he did by hiring a bunch of people from Caltech (where he had went to school) in Pasadena, while the other guy dealt with the business end of things up in Silicon Valley. The other guy ended up arranging things so that he got most of the money, our owner got almost nothing, and a lot of employees didn't get fully paid. Later that other guy was involved in other sleazy things in Silicon Valley, things where he ended up largely unscathed with others taking the blame or suffering the damage.
It was quite believable that he was behind that RAM double fraud and that the business people at their company did not know--that would be par for the course for him.
Anyway, we ended up going with them. They wanted a more showy interface, which we added, and they came up with a pretty cool name for the thing: Windrenalin. That got some good reviews [3].
I played around a bit to see if this same sort of thing would work on Linux. I used strace to log activity during several launches of Netscape. Then wrote a Perl script that looked at that, figured out what files were being read, and which parts of them were being read. It would then write out a shell script that consisted of a bunch of cat or dd commands to read the files or the parts of the files in an order that should leave the cache in a good state for Netscape, and then would exec Netscape.
Subsequently launching Netscape via that shell script on a freshly booted system was substantially faster than launching Netscape directly.
[1] That came about because a Japanese distributor asked us to make a Japanese version of an earlier product of ours, a CD-ROM cache. They gave the Japanese version a Japanese name. When we asked the what that name meant, they said "Superfassst!". Or CEO/Owner inexplicably liked that name, and slapped it on the hard disk accelerator.
[2] https://archive.org/stream/eu_BYTE-1997-03_OCR/BYTE-1997-03_...
What's interesting is the improvements made for a specific hardware situation can often end up slowing it down when the assumptions no longer apply (the most obvious example of this is the assumption about access times vs seek times applied to RAM vs spinning disk vs SSD vs NVME, etc).
If you're writing code "for the long term" and have optimization routines, it's good to have an option to disable them; perhaps in the future disk will be as fast as RAM (or someone's using a RAMDISK) and disabling optimizations for HDD will help.
Clearly ordering by block address is suboptimal on drives that remap blocks. Drives are also aware that certain seeks take longer than others, and that many reads take less than a full rotation.
It's a shame hard drives can't be trusted to just take all the requests and find a neqr-opyimal order to execute them in.
It would be interesting to try a version that used async IO to try and read literally all of the files at the same time and let the kernel send you the data as the disk spins to it.
Keeping a small window for sorting a subset of requests is much weaker than sorting all of them up front. Even if the window is 50% of all files, that would mean the average distance between requests increases 2 times (assuming blocks are distributed uniformly).
Maybe there are libraries that will do that. Or if not, project panama will make it easier to just call the relevant libc function.
Actually this is what fclones does, because it needs other metadata of the file as well (e.g. its length). I've found fetching metadata is lot faster than reading anything from a file, so most of the gains of this optimization are still there.
long ino = (Long)Files.getAttribute(file, "unix:ino");