Making CRDTs More Efficient
jakelazaroff.com
jakelazaroff.com
I'd also be interested to see what zstd (or even just gzip) would make of their original un-optimised format - I bet you'd get most of the way there "for free".
https://gist.github.com/jmfd/8dbb96fcd8a1ba1e8ad6e9167bd70ce...
And then tried these commands against it:
gzip: 39.56kb (93.7%)
zstd: 35.86kb (94.3%)
gzip -9: 35.51kb (94.4%)
bzip2: 24.35kb (96.1%)
zstd -19: 24.01kb (96.2%)
brotli: 21.42kb (96.6%)
(I'm not up-to-date on default web server configs, but I imagine most would automatically transmit with gzip over the wire for json?) gzip: 6.95kb (98.9%)
brotli: 5.99kb (99.0%) img: 13.552kb (100%)
img.png: 1.877kb (13.8%)
img.qoi: 10.316kb (76.1%)
img.qoi.gz: 1.167kb (8.6%)
While it does compress by 50%, it's not a format optimized for compression unlike png or qoiYou're also going to be relying on heuristics that may fail. Zstd has no idea that your UUIDs are going to repeat, it doesn't know to dictionary encode them or to create frequency mappings. What it ends up doing is unlikely to be as effective as doing the work ahead of time, then allowing zstd to find the interesting and complex patterns that arise out of your encoded format.
The article mentions RLE, which is also incredibly good for integer IDs if you diff-encode them, since ids are typically sequential with no or small gaps. Diff + RLE can turn your encoded structure into ~1 byte.
Also, incredible website. The interactivity is so fun.
I did something similar to this very recently where I took a dataset and just continuously applied data encoding methods to it. It was much smaller in memory and compressed with zstd to a smaller size as well. I've found that 'prepping' data before using a generalized compression algorithm has significant gains both for encode/decode performance + the output size. These were, incidentally, CRDT operations :D
Your blog posts are great, keep it up
This needs an explanation.
But for other integer datasets there's FastPFOR
https://github.com/lemire/FastPFor
The linked papers there will talk about techniques that can be used to store multiple 32bit integers into a single byte, etc. Integer compression is pretty powerful if your data isn't random. The thing with UUIDs is that your data is pretty random - even a UUIDv7 contains a significant amount of random data.
Edit: I see you're already linking to Daniel Lemire's github, so I guess you're familiar with them, just mentioning them, since it's in a comment thread like this one that I discovered them.
The current SOTA as I understand it is to have the timestamp be the most significant digits and a random value in the least (ULID, UUIDv7 etc). That way you get temporal locality for the UUID which is typically an indexed column where that helps for storage a bit (not so much for lookups because it’s still a random value relative to other concurrently accessed UIDs being input into the system)
I guess you’ll still have batches of locality depending on the uptime of the server which might be useful and generation would of course be significantly faster although I doubt many applications are really bottlenecks by UUID generation speed.
I don't quite get it. The normal string encoding of a UUID (d92b13b6-6780-4b98-82cc-d469dcdd42ab) clearly requires more bytes (36B) than the actual UUID itself (16B). The string encoding is for showing to users, it's not what you store in your DB, or your CRDT -- for that, you use the actual 16-byte value.
> Not only will your UUIDs take up 2x the space of a 64bit integer, they'll compress horribly by comparison. Integer compression is so good these days you can compress integers such that they basically end up taking up ~1/2 a byte instead of 8. Doing that with UUID v4s is fundamentally not going to work.
Yes, UUIDs are 128 bits, and 64 bit integers are 64 bits, but in both cases you're storing them as raw bytes -- or, maybe, integers, sure. Whatever compression works on your int64s will work just as well on your UUID int128s.
To reply to your initial point:
> Whatever compression works on your int64s will work just as well on your UUID int128s.
This is not the case. Compressing non-random 64bit values (such as the ones in the article - they are sequential) is going to be way more effective than compressing UUIDs. UUIDs do not compress, you'll likely end up with a larger output than the input.
What the article does, essentially, is a dictionary encoding pass. The duplicate, expensive UUIDs are mapped to small, sequential integers.
I explained elsewhere that you can turn nearly arbitrary numbers of sequential integers into a few bits each.
It's the situation we're presented with in the blog post. It's not an uncommon situation given that UUIDs are used in databases and, for many databases, compression is done at a block level.
> nor am I sure why this is assumed to be the primary metric for IDs.
It isn't.
That's not true in general; it depends on how the ints are distributed. UUIDs are, by design, distributed in a way that's pretty much maximally unfriendly to compression
Not that I think this optimization is unnecessary - I very much quite enjoy products that use CRDTs under the hood and undoubtedly had this sort of optimization and evolution to a binary format from JSON done. But I wanted to give a very brief but bigger system picture of where this fits.
The article is still neat, but more for the presentation. Of course you can get a huge reduction in data size if what you are starting out with is just about the most bloated representation you can imagine.
TL;DR brotli does the best — it compresses the JSON down to 21.42kb, which is a 96.6% reduction. That's probably the most bang for the buck. If you take the final binary encoding format from the end of the post, though, brotli crunches it down to 5.99kb — a 99% reduction!
Though I don’t think it’s super useful over the wire, LZ4 could also be an interesting thing to look at.
(Edit: I've taken out the "98%" bit per the HN guidelines: https://news.ycombinator.com/newsguidelines.html)
I wonder if it might be better to simply use PNG with an extra chunk for the UUID and timestamp information.
Building a collaborative pixel art editor with CRDTs - https://news.ycombinator.com/item?id=37832432 - Oct 2023 (23 comments)
An interactive intro to CRDTs - https://news.ycombinator.com/item?id=37764581 - Oct 2023 (130 comments)
Normally we downweight multipart posts after one has had significant frontpage attention (see https://hn.algolia.com/?dateRange=all&page=0&prefix=true&que... and https://hn.algolia.com/?dateRange=all&page=0&prefix=false&so... for why), but this seems to be something of an exception.
I'll see if I can find any performance gains. Sorry about that!
1. Palettes aka dictionary compression 2. Storing as column arrays + run length encoding 2. Binary encoding instead of ascii
All standard techniques used in modern compression, especially columnstore databases.
Very well written and approachable by a beginner.
Why does a completely full art board benefit from RLE that only tracks null pixels?
It switches data from row major / AoS to column major / SoA form, then each type gets its own RLE scheme, timestamp remains on RLE-ing only 0 values but color and writer switch to every value being RLE’d (so each entry is a pair of a value and a repetition count).