1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
//! This crate contains an implementation of the floating-point compression algorithm from the
//! paper ["ALP: Adaptive Lossless floating-Point Compression"][paper] by Afroozeh et al.
//!
//! The compressor has two variants: classic ALP, which is well-suited for data that does not use
//! the full precision, and "real doubles", for values that do.
//!
//! Classic ALP will return small integers, and it is meant to be cascaded with other integer
//! compression techniques such as bit-packing and frame-of-reference encoding. Combined, this
//! allows for significant compression on the order of what you can get for integer values.
//!
//! ALP-RD is generally terminal, and in the ideal case it can represent an f64 in just 49 bits,
//! though generally it is closer to 54 bits per value or ~12.5% compression.
//!
//! # ALP
//!
//! [`encode`] picks the best exponents for the input (unless they are given), and returns the
//! encoded integers alongside the positions and values of the exceptions that do not round-trip.
//! Exceptional slots in the encoded output hold a fill value, so that they stay in range for
//! downstream integer compression. Values are encoded in chunks of [`ENCODE_CHUNK_SIZE`], and one
//! offset per chunk is returned, so a consumer can find the exceptions of a chunk without scanning
//! the whole patch index.
//!
//! ```
//! let values = vec![1.234f64, 5.678, 9.0];
//! let (exponents, encoded, patch_indices, patch_values, chunk_offsets) =
//! alp::encode(&values, None);
//!
//! assert_eq!(encoded, vec![1234, 5678, 9000]);
//! assert!(patch_indices.is_empty() && patch_values.is_empty());
//! assert_eq!(chunk_offsets, vec![0]);
//! assert_eq!(alp::decode::<f64>(&encoded, exponents), values);
//! ```
//!
//! Decoding is usually done in place, over the encoded buffer itself
//! ([`decode_slice_inplace`]). If you plan to do that, encode with [`encode_into`] so the values
//! land in a buffer you own from the start, rather than in a `Vec` you would have to adopt:
//!
//! ```
//! # let values = vec![1.234f64, 5.678, 9.0];
//! let mut encoded: Vec<i64> = Vec::with_capacity(values.len());
//! let (mut patch_indices, mut patch_values, mut chunk_offsets) = (vec![], vec![], vec![]);
//!
//! let exponents = alp::encode_into(
//! &values,
//! None,
//! &mut encoded.spare_capacity_mut()[..values.len()],
//! &mut patch_indices,
//! &mut patch_values,
//! &mut chunk_offsets,
//! );
//! // SAFETY: `encode_into` initializes one element per value.
//! unsafe { encoded.set_len(values.len()) };
//!
//! alp::decode_slice_inplace::<f64>(&mut encoded, exponents);
//! ```
//!
//! # ALP-RD
//!
//! [`RDEncoder`] derives a dictionary of the most common front bits from a sample, and then splits
//! values into dictionary-encoded left parts and bit-packable right parts. Left parts that are not
//! in the dictionary are stored as exceptions.
//!
//! ```
//! let values = vec![0.1f64, 0.2, 3e100];
//! let encoder = alp::RDEncoder::new(&values[0..2]);
//!
//! let split = encoder.split(&values);
//! assert_eq!(split.left_exceptions().positions(), &[2]);
//! assert_eq!(split.decode(), values);
//! ```
//!
//! # Bit-packing
//!
//! [fastlanes] packs 1024 values at a time, which is also [`ENCODE_CHUNK_SIZE`], so encode and pack
//! at the same granularity: a chunk at a time. A shorter chunk is padded out to 1024 values, so
//! packing a handful of values saves nothing.
//!
//! ## Bit-packing ALP
//!
//! Classic ALP's encoded integers are signed and sit wherever the data does, so packing them starts
//! with a frame of reference: subtract the minimum, pack the differences, keep the minimum. Patched
//! slots hold a fill value taken from the data itself, so they do not widen that frame.
//!
//! | Component | Where | Count | Pack at |
//! | --- | --- | --- | --- |
//! | encoded values | [`encode`]'s 2nd result | one per value | [`bit_width`] of `max - min`, after subtracting `min` |
//! | patch positions | [`encode`]'s 3rd result | one per exception | as is, or 10 bits for a 1024-value chunk |
//! | patch values | [`encode`]'s 4th result | one per exception | as is: they are the original floats, at full precision |
//! | chunk offsets | [`encode`]'s 5th result | one per [`ENCODE_CHUNK_SIZE`] | as is |
//! | exponents | [`encode`]'s 1st result | two bytes per column | as is |
//!
//! ```
//! # use fastlanes::BitPacking;
//! # fn pack<T: BitPacking>(chunk: &[T; 1024], width: usize) -> Vec<T> {
//! # let mut packed = vec![T::zero(); 1024 * width / (size_of::<T>() * 8)];
//! # unsafe { BitPacking::unchecked_pack(width, chunk, &mut packed) };
//! # packed
//! # }
//! # fn unpack<T: BitPacking>(packed: &[T], width: usize) -> Vec<T> {
//! # let mut chunk = vec![T::zero(); 1024];
//! # unsafe { BitPacking::unchecked_unpack(width, packed, &mut chunk) };
//! # chunk
//! # }
//! // Two decimal places, which is what classic ALP is for.
//! let mut values: Vec<f64> = (0..1024).map(|i| 100.0 + (i % 500) as f64 / 100.0).collect();
//! values[7] = 1.0 / 3.0; // Does not round-trip, so it becomes a patch.
//!
//! let (exponents, encoded, patch_positions, patch_values, _chunk_offsets) =
//! alp::encode(&values, None);
//!
//! // Frame of reference, then pack the differences.
//! let min = encoded.iter().copied().min().expect("a non-empty chunk");
//! let max = encoded.iter().copied().max().expect("a non-empty chunk");
//! let width = alp::bit_width(max.wrapping_sub(min) as u64) as usize;
//!
//! let shifted: Vec<u64> = encoded.iter().map(|v| v.wrapping_sub(min) as u64).collect();
//! let shifted: [u64; 1024] = shifted.try_into().expect("one full chunk");
//! let packed = pack(&shifted, width);
//!
//! // 1,152 bytes where the input took 8,192, or 9 bits per value: the encoded integers run from
//! // 10000 to 10499, and the patched slot holds a fill value inside that range.
//! assert!(size_of_val(packed.as_slice()) < size_of_val(values.as_slice()));
//!
//! // Undo the frame, decode, and write the patches over the slots that did not round-trip.
//! let unshifted: Vec<i64> = unpack(&packed, width)
//! .into_iter()
//! .map(|v| (v as i64).wrapping_add(min))
//! .collect();
//!
//! let mut decoded = alp::decode::<f64>(&unshifted, exponents);
//! for (&position, &value) in patch_positions.iter().zip(&patch_values) {
//! decoded[position as usize] = value;
//! }
//! assert_eq!(decoded, values);
//! ```
//!
//! ## Bit-packing ALP-RD
//!
//! A [`Split`] is five arrays. The two per-value ones are what bit-packing is for:
//!
//! | Component | Where | Count | Pack at |
//! | --- | --- | --- | --- |
//! | left parts (dictionary codes) | [`Split::left_parts`] | one per value | [`Split::left_parts_bit_width`]: 1 to 3 bits |
//! | right parts | [`Split::right_parts`] | one per value | [`Split::right_parts_bit_width`]: 48 to 63 bits for an `f64` |
//! | dictionary | [`Split::left_dict`] | at most [`MAX_DICT_SIZE`] `u16`s | as is — it belongs to the encoder, so it is shared by every chunk that encoder splits |
//! | exception patterns | [`Exceptions::values`] | one per exception | as is: at most 16 bits already |
//! | exception positions | [`Exceptions::positions`] | one per exception | as is, or 10 bits for a 1024-value chunk, if a chunk has enough exceptions to pay for a pack |
//!
//! ```
//! use fastlanes::BitPacking;
//!
//! /// Packs one 1024-value chunk down to `width` bits per value.
//! fn pack<T: BitPacking>(chunk: &[T; 1024], width: usize) -> Vec<T> {
//! let mut packed = vec![T::zero(); 1024 * width / (size_of::<T>() * 8)];
//! // SAFETY: a full chunk in, exactly as many words out as 1024 values of `width` bits
//! // occupy, and `width` is no wider than `T`.
//! unsafe { BitPacking::unchecked_pack(width, chunk, &mut packed) };
//! packed
//! }
//!
//! /// Unpacks a chunk `pack` produced.
//! fn unpack<T: BitPacking>(packed: &[T], width: usize) -> Vec<T> {
//! let mut chunk = vec![T::zero(); 1024];
//! // SAFETY: as above, in reverse.
//! unsafe { BitPacking::unchecked_unpack(width, packed, &mut chunk) };
//! chunk
//! }
//!
//! // "Real doubles": these use the full mantissa, so classic ALP has nothing to work with.
//! let values: Vec<f64> = (0..1024).map(|i| (i as f64 + 0.5).sqrt()).collect();
//!
//! let encoder = alp::RDEncoder::new(&values);
//! let split = encoder.split(&values);
//!
//! let (left_width, right_width) = (
//! split.left_parts_bit_width() as usize,
//! split.right_parts_bit_width() as usize,
//! );
//! let left: [u16; 1024] = split.left_parts().try_into().expect("one full chunk");
//! let right: [u64; 1024] = split.right_parts().try_into().expect("one full chunk");
//!
//! let packed_left = pack(&left, left_width);
//! let packed_right = pack(&right, right_width);
//!
//! // Those two arrays are the whole payload: 6,912 bytes where the input took 8,192, or 54 bits
//! // per value — 51 for a right part, 3 for a dictionary code.
//! let nbytes = size_of_val(packed_left.as_slice()) + size_of_val(packed_right.as_slice());
//! assert!(nbytes < size_of_val(values.as_slice()));
//!
//! // Decoding takes the two parts unpacked, the encoder's dictionary, and the exceptions as they
//! // came.
//! let decoded = alp::alp_rd_decode::<f64>(
//! &unpack(&packed_left, left_width),
//! split.left_dict(),
//! split.right_parts_bit_width(),
//! &unpack(&packed_right, right_width),
//! split.left_exceptions().positions(),
//! split.left_exceptions().values(),
//! );
//! assert_eq!(decoded, values);
//!
//! // Random access survives packing: one value costs one unpacking operation per part, plus a look
//! // at the exceptions, whose positions come out ascending.
//! let position = 512;
//! let left = match split.left_exceptions().positions().binary_search(&(position as u64)) {
//! Ok(exception) => split.left_exceptions().values()[exception],
//! Err(_) => {
//! let code = unsafe { u16::unchecked_unpack_single(left_width, &packed_left, position) };
//! split.left_dict()[code as usize]
//! }
//! };
//! let right = unsafe { u64::unchecked_unpack_single(right_width, &packed_right, position) };
//! assert_eq!(
//! f64::from_bits((u64::from(left) << right_width) | right),
//! values[position],
//! );
//! ```
//!
//! [paper]: https://ir.cwi.nl/pub/33334/33334.pdf
//! [fastlanes]: https://docs.rs/fastlanes
//! [vortex]: https://docs.rs/vortex
pub use *;
pub use *;
// Runs the README's examples as doctests, so they cannot drift from the API they demonstrate.
;
/// A sparse vector containing exceptions to the encoding process.
///
/// When either of the ALP variants encounters values it is unable to compress, they are stored
/// here using the actual encoding offsets instead.
///
/// Positions are indices into the array that was encoded, in ascending order, so they can be
/// searched for the exception belonging to a value. [The section on bit-packing](crate#bit-packing)
/// covers how to store them.