63 lines
3.4 KiB
Markdown
63 lines
3.4 KiB
Markdown
|
# bstream details
|
||
|
|
||
|
This doc describes details of the bstream (bitstream) and how we use it for encoding and decoding.
|
||
|
This doc is incomplete. For more background, see the Gorilla TSDB [white paper](http://www.vldb.org/pvldb/vol8/p1816-teller.pdf)
|
||
|
or the original [go-tsz](https://github.com/dgryski/go-tsz) implementation, which this code is based on.
|
||
|
|
||
|
## Delta-of-delta encoding for timestamps
|
||
|
|
||
|
We need to be able to encode and decode dod's for timestamps, which can be positive, zero, or negative.
|
||
|
Note that int64's are implemented as [2's complement](https://en.wikipedia.org/wiki/Two%27s_complement)
|
||
|
|
||
|
and look like:
|
||
|
|
||
|
```
|
||
|
0111...111 = maxint64
|
||
|
...
|
||
|
0000...111 = 7
|
||
|
0000...110 = 6
|
||
|
0000...101 = 5
|
||
|
0000...100 = 4
|
||
|
0000...011 = 3
|
||
|
0000...010 = 2
|
||
|
0000...001 = 1
|
||
|
0000...000 = 0
|
||
|
1111...111 = -1
|
||
|
1111...110 = -2
|
||
|
1111...101 = -3
|
||
|
1111...100 = -4
|
||
|
1111...011 = -5
|
||
|
1111...010 = -6
|
||
|
1111...001 = -7
|
||
|
1111...000 = -8
|
||
|
...
|
||
|
1000...001 = minint64+1
|
||
|
1000...000 = minint64
|
||
|
```
|
||
|
|
||
|
All numbers have a prefix (of zeroes for positive numbers, of ones for negative numbers), followed by a number of significant digits at the end.
|
||
|
In all cases, the smaller the absolute value of the number, the fewer the amount of significant digits.
|
||
|
|
||
|
To encode these numbers, we use:
|
||
|
* A prefix which declares the amount of bits that follow (we use a predefined list of options in order of increasing number of significant bits).
|
||
|
* A number of bits which is one more than the number of significant bits. The extra bit is needed because we deal with unsigned integers, although
|
||
|
it isn't exactly a sign bit. (See below for details).
|
||
|
|
||
|
The `bitRange` function determines whether a given integer can be represented by a number of bits.
|
||
|
For a given number of bits `nbits` we can distinguish (and thus encode) any set of `2^nbits` numbers.
|
||
|
E.g. for `nbits = 3`, we can encode 8 distinct numbers, and we have a choice of choosing our boundaries. For example -4 to 3,
|
||
|
-3 to 4, 0 to 7 or even -2 to 5 (always inclusive). (Observe in the list above that this is always true.)
|
||
|
Because we need to support positive and negative numbers equally, we choose boundaries that grow symmetrically. Following the same example,
|
||
|
we choose -3 to 4.
|
||
|
|
||
|
When decoding the number, the most interesting part is how to recognize whether a number is negative or positive, and thus which prefix to set.
|
||
|
Note that the bstream library doesn't interpret integers to a specific type, but rather returns them as uint64's (which are really just a container for 64 bits).
|
||
|
Within the ranges we choose, if looked at as unsigned integers, the higher portion of the range represent the negative numbers.
|
||
|
Continuing the same example, the numbers 001, 010, 011 and 100 are returned as unsigned integers 1,2,3,4 and mean the same thing when casted to int64's.
|
||
|
But the others, 101, 110 and 111 are returned as unsigned integers 5,6,7 but actually represent -3, -2 and -1 (see list above),
|
||
|
The cutoff value is the value set by the `nbit`'th bit, and needs a value subtracted that is represented by the `nbit+1`th bit.
|
||
|
In our example, the 3rd bit sets the number 4, and the 4th sets the number 8. So if we see an unsigned integer exceeding 4 (5,6,7) we subtract 8. This gives us our desired values (-3, -2 and -1).
|
||
|
|
||
|
Careful observers may note that, if we shift our boundaries down by one, the first bit would always indicate the sign (and imply the needed prefix).
|
||
|
In our example of `nbits = 3`, that would mean the range from -4 to 3. But what we have now works just fine too.
|