XOR crushes adjacent floats

Adjacent float64 samples from the same series share most of their sign/exponent/high-mantissa bits, so XOR-ing them yields long runs of leading and trailing zeros — and an unchanged value collapses to a single bit.

Previously

Timestamps shrunk because they have a clock. Floats from the same series have their own kind of structure — the bits, not the values, are nearly stationary.

Scene 04

XOR crushes adjacent floats

  1. Watch
  2. Try it
  3. Predict
  4. Capture
XOR of two float64 samples — control prefix `0`BITS SO FAR0 / 64exponent (11)mantissa (52)prev = 42.00100000001000101000000000000000000000000000000000000000000000000curr = 42.00100000001000101000000000000000000000000000000000000000000000000prev XOR curr0000000000000000000000000000000000000000000000000000000000000000encoded (0)0Three pairs incoming. Watch the XOR row cancel almost everything when prev ≈ curr.Naive: 64 bits / pointEncoded so far: 0 bits
What to watch for

Two adjacent scrapes of the same gauge usually report numbers that are almost identical. If you XOR them, most of the bits cancel — the leftover bits are tiny and easy to encode. The name for this is XOR encoding. Watch three pairs walk through it.

Continue unlocks when the animation finishes.
Implementation

Highlighted lines are the ones running in the diagram right now.

Encoder.encodeXorValue
dispatch on the XOR; one of three control prefixes
def encodeXorValue(prev, curr, prev_window):
xor = float64_bits(prev) ^ float64_bits(curr)
if xor == 0:
write_bit(0) # control '0'
return prev_window
lead, length, body = meaningfulBits(xor)
if fits_in(prev_window, lead, length):
write_bits(0b10, 2) # control '10'
write_bits(body, prev_window.length)
return prev_window
write_bits(0b11, 2) # control '11'
write_bits(lead, 5) # 5-bit leading zeros
write_bits(length, 6) # 6-bit meaningful length
write_bits(body, length)
return Window(lead, length)
Encoder.meaningfulBits
trim leading and trailing zeros from the XOR
def meaningfulBits(xor):
# adjacent float64s share sign/exponent/high mantissa,
# so xor has long runs of zeros at both ends.
lead = clz64(xor) # count leading zeros
trail = ctz64(xor) # count trailing zeros
length = 64 - lead - trail # meaningful bit count
body = (xor >> trail) & ((1 << length) - 1)
return (lead, length, body)
Decoder.decodeXorValue
symmetric reader; control prefix selects the path
def decodeXorValue(prev, prev_window):
if read_bit() == 0:
return prev # unchanged
if read_bit() == 0: # control '10'
body = read_bits(prev_window.length)
xor = body << prev_window.trail
return from_bits(float64_bits(prev) ^ xor)
lead = read_bits(5) # control '11'
length = read_bits(6)
body = read_bits(length)
xor = body << (64 - lead - length)
return from_bits(float64_bits(prev) ^ xor)

Where this sits in Build a Prometheus-style time-series database

Scene 04 of 12. Adjacent float64s share most of their IEEE-754 bits. XOR them and the leftover bits are tiny — an unchanged value costs 1 bit.

Up next. Each technique wins on its axis. The real question is: stitched together, what does one unit of compressed data look like on disk?

All 12 scenes in Build a Prometheus-style time-series database · Every curriculum

Built with Arqly
Every scene in Build a Prometheus-style time-series database builds on the one before it.All 12 Build a Prometheus-style time-series database scenes