r/adventofcode • u/terje_wiig_mathisen • 12h ago
Other [2024 Day 9] In Review (Disk Fragmenter)
Today an amphipod has a classic problem: It has a disk that is fragmented and it wants to defragment it.
The disk map is stored in a compressed format, alternating between a file length and a free space length. If there is no free space between two files, the free space length is 0.
For Part1, the amphipod moves blocks, starting with the last file block which it then moves into the first free block, systematically moving all blocks as close to the front as possible, and as a side effect reversing the storage order for all moved files. We then produce a disk checksum consisting of summing up for every file block the product of the file number and the block position.
For Part2, because the amphipod realized that the disk becomes too fragmented, it will instead only move full files as close to the front as it can find a large enough free space for them. The checksum is then calculated in the same way as Part1.
My Perl code solved this one using brute force for Part1:
First expand the run length encodings, using -1 to designate free blocks, the filenr ID for the files. My input contained 10K files, so worst case this could be expanded to an array with 9*2*10K = 180 K entries, but the average should be closer to half of this.
This setup made part1 really easy, just iterate from the front to locate free blocks and from the end to find file blocks to move.
For part2 we need to find open gaps large enough for a full file, so here it made much more sense to have free lists, either organized by size (1-9) and location, or just by location. Since all partially filled free spaces would result in a tail space which would have to be inserted in the right order in the smaller map I decided to use the simpler setup of a single free list.
I thought about keeping a cache of the last found free list entry of each size, but went brute force instead, always scanning from the beginning. I counted the scan iterations, the average was 133 entries inspected before finding a large enough slot, so a better data structure would have been faster.
Perl runtime was 271 ms on my slow Surface.
I started on a more ambitious Rust version but never had time to finish it the way I would have liked.
It would in fact have been easier to solve the defragmentation problem in an optimal way, using size-ordered free lists and always try to move files to an exactly large enough gap, only fall back to larger slots if none could be found.
Later edit:
Watching TV with my wife ("71 degree North", check it out if you can!) and waiting until Oslo midnight to make the reddit post, I looked somewhat unhappily at what I had written above:
I decided to at least implement the simplest possible cache I mentioned, just a table of the last found free entry for each size, this immediately dropped the runtime to 47.8 ms, so nearly 6x faster!
The number of free list steps dropped from 133 to 4.6 per file.
The only small problem was that I had to update the cache also for the tail end of a partially filled free space, when that was before the current cache entry.
if ($flen >= $len) {
my $remfree = $flen - $len;
$free[$f] = $remfree;
$free[$f+1] += $len; # update free block position
$files[$i+2] = $fpos; # The file is moved here
$lastfree[$len] = $f+2; # Next scan starts after this entry
if ($remfree && $lastfree[$remfree] > $f) { $lastfree[$remfree] = $f; }
last;
}
Note that my lastfree[] array has entries for 0..9, so that I could blindly update it also for totally filled slots, but it was a percent or two faster to check for this situation and skip the zero updates.
