Skip to content

Commit 60d9626

Browse files
committed
Lazily load in BitpackedCodec in preallocated blocks.
1 parent 16740ad commit 60d9626

5 files changed

Lines changed: 161 additions & 30 deletions

File tree

bitpacker/src/bitpacker.rs

Lines changed: 86 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,12 @@ pub struct BitUnpacker {
6969
mask: u64,
7070
}
7171

72+
pub type BlockNumber = usize;
73+
74+
// 16k
75+
const BLOCK_SIZE_MIN_POW: u8 = 14;
76+
const BLOCK_SIZE_MIN: usize = 2 << BLOCK_SIZE_MIN_POW;
77+
7278
impl BitUnpacker {
7379
/// Creates a bit unpacker, that assumes the same bitwidth for all values.
7480
///
@@ -82,6 +88,7 @@ impl BitUnpacker {
8288
} else {
8389
(1u64 << num_bits) - 1u64
8490
};
91+
8592
BitUnpacker {
8693
num_bits: u32::from(num_bits),
8794
mask,
@@ -92,10 +99,63 @@ impl BitUnpacker {
9299
self.num_bits as u8
93100
}
94101

102+
/// Calculates a block number for the given `idx`.
103+
#[inline]
104+
pub fn block_num(&self, idx: u32) -> BlockNumber {
105+
// Find the address in bits of the index.
106+
let addr_in_bits = (idx * self.num_bits) as usize;
107+
108+
// Then round down to the nearest byte.
109+
let addr_in_bytes = addr_in_bits >> 3;
110+
111+
// And compute the containing BlockNumber.
112+
addr_in_bytes >> (BLOCK_SIZE_MIN_POW + 1)
113+
}
114+
115+
/// Given a block number and dataset length, calculates a data Range for the block.
116+
pub fn block(&self, block: BlockNumber, data_len: usize) -> Range<usize> {
117+
let block_addr = block << (BLOCK_SIZE_MIN_POW + 1);
118+
// We extend the end of the block by a constant factor, so that it overlaps the next
119+
// block. That ensures that we never need to read on a block boundary.
120+
block_addr..(std::cmp::min(block_addr + BLOCK_SIZE_MIN + 8, data_len))
121+
}
122+
123+
/// Calculates the number of blocks for the given data_len.
124+
///
125+
/// Usually only called at startup to pre-allocate structures.
126+
pub fn block_count(&self, data_len: usize) -> usize {
127+
let block_count = data_len / (BLOCK_SIZE_MIN as usize);
128+
if data_len % (BLOCK_SIZE_MIN as usize) == 0 {
129+
block_count
130+
} else {
131+
block_count + 1
132+
}
133+
}
134+
135+
/// Returns a range within the data which covers the given id_range.
136+
///
137+
/// NOTE: This method is used for batch reads which bypass blocks to avoid dealing with block
138+
/// boundaries.
139+
#[inline]
140+
pub fn block_oblivious_range(&self, id_range: Range<u32>, data_len: usize) -> Range<usize> {
141+
let start_in_bits = id_range.start * self.num_bits;
142+
let start = (start_in_bits >> 3) as usize;
143+
let end_in_bits = id_range.end * self.num_bits;
144+
let end = (end_in_bits >> 3) as usize;
145+
// TODO: We fetch more than we need and then truncate.
146+
start..(std::cmp::min(end + 8, data_len))
147+
}
148+
95149
#[inline]
96150
pub fn get(&self, idx: u32, data: &[u8]) -> u64 {
151+
self.get_from_subset(idx, 0, data)
152+
}
153+
154+
/// Get the value at the given idx, which must exist within the given subset of the data.
155+
#[inline]
156+
pub fn get_from_subset(&self, idx: u32, data_offset: usize, data: &[u8]) -> u64 {
97157
let addr_in_bits = idx * self.num_bits;
98-
let addr = (addr_in_bits >> 3) as usize;
158+
let addr = (addr_in_bits >> 3) as usize - data_offset;
99159
if addr + 8 > data.len() {
100160
if self.num_bits == 0 {
101161
return 0;
@@ -129,7 +189,7 @@ impl BitUnpacker {
129189
// #Panics
130190
//
131191
// This methods panics if `num_bits` is > 32.
132-
fn get_batch_u32s(&self, start_idx: u32, data: &[u8], output: &mut [u32]) {
192+
fn get_batch_u32s(&self, start_idx: u32, data_offset: usize, data: &[u8], output: &mut [u32]) {
133193
assert!(
134194
self.bit_width() <= 32,
135195
"Bitwidth must be <= 32 to use this method."
@@ -140,14 +200,14 @@ impl BitUnpacker {
140200
let end_bit_read = end_idx * self.num_bits;
141201
let end_byte_read = (end_bit_read + 7) / 8;
142202
assert!(
143-
end_byte_read as usize <= data.len(),
203+
end_byte_read as usize <= data_offset + data.len(),
144204
"Requested index is out of bounds."
145205
);
146206

147207
// Simple slow implementation of get_batch_u32s, to deal with our ramps.
148208
let get_batch_ramp = |start_idx: u32, output: &mut [u32]| {
149209
for (out, idx) in output.iter_mut().zip(start_idx..) {
150-
*out = self.get(idx, data) as u32;
210+
*out = self.get_from_subset(idx, data_offset, data) as u32;
151211
}
152212
};
153213

@@ -177,7 +237,7 @@ impl BitUnpacker {
177237
get_batch_ramp(start_idx, &mut output[..entrance_ramp_len as usize]);
178238

179239
// Highway
180-
let mut offset = (highway_start * self.num_bits) as usize / 8;
240+
let mut offset = ((highway_start * self.num_bits) as usize / 8) - data_offset;
181241
let mut output_cursor = (highway_start - start_idx) as usize;
182242
for _ in 0..num_blocks {
183243
offset += BitPacker1x.decompress(
@@ -199,31 +259,43 @@ impl BitUnpacker {
199259
id_range: Range<u32>,
200260
data: &[u8],
201261
positions: &mut Vec<u32>,
262+
) {
263+
self.get_ids_for_value_range_from_subset(range, id_range, 0, data, positions)
264+
}
265+
266+
pub fn get_ids_for_value_range_from_subset(
267+
&self,
268+
range: RangeInclusive<u64>,
269+
id_range: Range<u32>,
270+
data_offset: usize,
271+
data: &[u8],
272+
positions: &mut Vec<u32>,
202273
) {
203274
if self.bit_width() > 32 {
204-
self.get_ids_for_value_range_slow(range, id_range, data, positions)
275+
self.get_ids_for_value_range_slow(range, id_range, data_offset, data, positions)
205276
} else {
206277
if *range.start() > u32::MAX as u64 {
207278
positions.clear();
208279
return;
209280
}
210281
let range_u32 = (*range.start() as u32)..=(*range.end()).min(u32::MAX as u64) as u32;
211-
self.get_ids_for_value_range_fast(range_u32, id_range, data, positions)
282+
self.get_ids_for_value_range_fast(range_u32, id_range, data_offset, data, positions)
212283
}
213284
}
214285

215286
fn get_ids_for_value_range_slow(
216287
&self,
217288
range: RangeInclusive<u64>,
218289
id_range: Range<u32>,
290+
data_offset: usize,
219291
data: &[u8],
220292
positions: &mut Vec<u32>,
221293
) {
222294
positions.clear();
223295
for i in id_range {
224296
// If we cared we could make this branchless, but the slow implementation should rarely
225297
// kick in.
226-
let val = self.get(i, data);
298+
let val = self.get_from_subset(i, data_offset, data);
227299
if range.contains(&val) {
228300
positions.push(i);
229301
}
@@ -234,11 +306,12 @@ impl BitUnpacker {
234306
&self,
235307
value_range: RangeInclusive<u32>,
236308
id_range: Range<u32>,
309+
data_offset: usize,
237310
data: &[u8],
238311
positions: &mut Vec<u32>,
239312
) {
240313
positions.resize(id_range.len(), 0u32);
241-
self.get_batch_u32s(id_range.start, data, positions);
314+
self.get_batch_u32s(id_range.start, data_offset, data, positions);
242315
crate::filter_vec::filter_vec_in_place(value_range, id_range.start, positions)
243316
}
244317
}
@@ -329,14 +402,14 @@ mod test {
329402
fn test_get_batch_panics_over_32_bits() {
330403
let bitunpacker = BitUnpacker::new(33);
331404
let mut output: [u32; 1] = [0u32];
332-
bitunpacker.get_batch_u32s(0, &[0, 0, 0, 0, 0, 0, 0, 0], &mut output[..]);
405+
bitunpacker.get_batch_u32s(0, 0, &[0, 0, 0, 0, 0, 0, 0, 0], &mut output[..]);
333406
}
334407

335408
#[test]
336409
fn test_get_batch_limit() {
337410
let bitunpacker = BitUnpacker::new(1);
338411
let mut output: [u32; 3] = [0u32, 0u32, 0u32];
339-
bitunpacker.get_batch_u32s(8 * 4 - 3, &[0u8, 0u8, 0u8, 0u8], &mut output[..]);
412+
bitunpacker.get_batch_u32s(8 * 4 - 3, 0, &[0u8, 0u8, 0u8, 0u8], &mut output[..]);
340413
}
341414

342415
#[test]
@@ -345,7 +418,7 @@ mod test {
345418
let bitunpacker = BitUnpacker::new(1);
346419
let mut output: [u32; 3] = [0u32, 0u32, 0u32];
347420
// We are missing exactly one bit.
348-
bitunpacker.get_batch_u32s(8 * 4 - 2, &[0u8, 0u8, 0u8, 0u8], &mut output[..]);
421+
bitunpacker.get_batch_u32s(8 * 4 - 2, 0, &[0u8, 0u8, 0u8, 0u8], &mut output[..]);
349422
}
350423

351424
proptest::proptest! {
@@ -368,7 +441,7 @@ mod test {
368441
for len in [0, 1, 2, 32, 33, 34, 64] {
369442
for start_idx in 0u32..32u32 {
370443
output.resize(len, 0);
371-
bitunpacker.get_batch_u32s(start_idx, &buffer, &mut output);
444+
bitunpacker.get_batch_u32s(start_idx, 0, &buffer, &mut output);
372445
for (i, output_byte) in output.iter().enumerate() {
373446
let expected = (start_idx + i as u32) & mask;
374447
assert_eq!(*output_byte, expected);

columnar/src/column_values/stats.rs

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@ use std::io;
22
use std::io::Write;
33
use std::num::NonZeroU64;
44

5-
use common::{BinarySerializable, VInt};
5+
use common::file_slice::FileSlice;
6+
use common::{BinarySerializable, HasLen, VInt};
67

78
use crate::RowId;
89

@@ -28,9 +29,21 @@ impl ColumnStats {
2829
}
2930

3031
impl ColumnStats {
32+
/// Deserialize from the tail of the given FileSlice, and return the stats and remaining prefix
33+
/// FileSlice.
34+
pub fn deserialize_from_tail(file_slice: FileSlice) -> io::Result<(Self, FileSlice)> {
35+
// [`deserialize_with_size`] deserializes 4 variable-width encoded u64s, which
36+
// could end up being, in the worst case, 9 bytes each. this is where the 36 comes from
37+
let (stats, _) = file_slice.clone().split(36.min(file_slice.len())); // hope that's enough bytes
38+
let mut stats = stats.read_bytes()?;
39+
let (stats, stats_nbytes) = ColumnStats::deserialize_with_size(&mut stats)?;
40+
let (_, remainder) = file_slice.split(stats_nbytes);
41+
Ok((stats, remainder))
42+
}
43+
3144
/// Same as [`BinarySeerializable::deserialize`] but also returns the number of bytes
3245
/// consumed from the reader `R`
33-
pub fn deserialize_with_size<R: io::Read>(reader: &mut R) -> io::Result<(Self, usize)> {
46+
fn deserialize_with_size<R: io::Read>(reader: &mut R) -> io::Result<(Self, usize)> {
3447
let mut nbytes = 0;
3548

3649
let (min_value, len) = VInt::deserialize_with_size(reader)?;

columnar/src/column_values/u64_based/bitpacked.rs

Lines changed: 58 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
use std::io::{self, Write};
22
use std::num::NonZeroU64;
33
use std::ops::{Range, RangeInclusive};
4+
use std::sync::{Arc, OnceLock};
45

56
use common::file_slice::FileSlice;
6-
use common::{BinarySerializable, OwnedBytes};
7+
use common::{BinarySerializable, HasLen, OwnedBytes};
78
use fastdivide::DividerU64;
89
use tantivy_bitpacker::{compute_num_bits, BitPacker, BitUnpacker};
910

@@ -14,9 +15,40 @@ use crate::{ColumnValues, RowId};
1415
/// fast field is required.
1516
#[derive(Clone)]
1617
pub struct BitpackedReader {
17-
data: OwnedBytes,
18+
data: FileSlice,
1819
bit_unpacker: BitUnpacker,
1920
stats: ColumnStats,
21+
blocks: Arc<[OnceLock<Block>]>,
22+
}
23+
24+
impl BitpackedReader {
25+
#[inline(always)]
26+
fn unpack_val(&self, doc: u32) -> u64 {
27+
let block_num = self.bit_unpacker.block_num(doc);
28+
29+
if block_num == 0 && self.blocks.len() == 0 {
30+
return 0;
31+
}
32+
33+
let block = self.blocks[block_num].get_or_init(|| {
34+
let block_range = self.bit_unpacker.block(block_num, self.data.len());
35+
let offset = block_range.start;
36+
let data = self
37+
.data
38+
.slice(block_range)
39+
.read_bytes()
40+
.expect("Failed to read column values.");
41+
Block { offset, data }
42+
});
43+
44+
self.bit_unpacker
45+
.get_from_subset(doc, block.offset, &block.data)
46+
}
47+
}
48+
49+
struct Block {
50+
offset: usize,
51+
data: OwnedBytes,
2052
}
2153

2254
#[inline(always)]
@@ -62,8 +94,9 @@ fn transform_range_before_linear_transformation(
6294
impl ColumnValues for BitpackedReader {
6395
#[inline(always)]
6496
fn get_val(&self, doc: u32) -> u64 {
65-
self.stats.min_value + self.stats.gcd.get() * self.bit_unpacker.get(doc, &self.data)
97+
self.stats.min_value + self.stats.gcd.get() * self.unpack_val(doc)
6698
}
99+
67100
#[inline]
68101
fn min_value(&self) -> u64 {
69102
self.stats.min_value
@@ -89,10 +122,23 @@ impl ColumnValues for BitpackedReader {
89122
positions.clear();
90123
return;
91124
};
92-
self.bit_unpacker.get_ids_for_value_range(
125+
// TODO: This does not use the `self.blocks` cache, because callers are usually already
126+
// doing sequential, and fairly dense reads. Fix it to iterate over blocks if that
127+
// assumption turns out to be incorrect!
128+
let data_range = self
129+
.bit_unpacker
130+
.block_oblivious_range(doc_id_range.clone(), self.data.len());
131+
let data_offset = data_range.start;
132+
let data_subset = self
133+
.data
134+
.slice(data_range)
135+
.read_bytes()
136+
.expect("Failed to read column values.");
137+
self.bit_unpacker.get_ids_for_value_range_from_subset(
93138
transformed_range,
94139
doc_id_range,
95-
&self.data,
140+
data_offset,
141+
&data_subset,
96142
positions,
97143
);
98144
}
@@ -139,14 +185,19 @@ impl ColumnCodec for BitpackedCodec {
139185

140186
/// Opens a fast field given a file.
141187
fn load(file_slice: FileSlice) -> io::Result<Self::ColumnValues> {
142-
let mut data = file_slice.read_bytes()?;
143-
let stats = ColumnStats::deserialize(&mut data)?;
188+
let (stats, data) = ColumnStats::deserialize_from_tail(file_slice)?;
189+
144190
let num_bits = num_bits(&stats);
145191
let bit_unpacker = BitUnpacker::new(num_bits);
192+
let block_count = bit_unpacker.block_count(data.len());
146193
Ok(BitpackedReader {
147194
data,
148195
bit_unpacker,
149196
stats,
197+
blocks: (0..block_count)
198+
.into_iter()
199+
.map(|_| OnceLock::new())
200+
.collect(),
150201
})
151202
}
152203
}

columnar/src/column_values/u64_based/blockwise_linear.rs

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -175,13 +175,7 @@ impl ColumnCodec<u64> for BlockwiseLinearCodec {
175175
type Estimator = BlockwiseLinearEstimator;
176176

177177
fn load(file_slice: FileSlice) -> io::Result<Self::ColumnValues> {
178-
// [`ColumnStats::deserialize_with_size`] deserializes 4 variable-width encoded u64s, which
179-
// could end up being, in the worst case, 9 bytes each. this is where the 36 comes from
180-
let (stats, _) = file_slice.clone().split(36.min(file_slice.len())); // hope that's enough bytes
181-
let mut stats = stats.read_bytes()?;
182-
let (stats, stats_nbytes) = ColumnStats::deserialize_with_size(&mut stats)?;
183-
184-
let (_, body) = file_slice.split(stats_nbytes);
178+
let (stats, body) = ColumnStats::deserialize_from_tail(file_slice)?;
185179

186180
let (_, footer) = body.clone().split_from_end(4);
187181

src/fastfield/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -395,7 +395,7 @@ mod tests {
395395
.unwrap()
396396
.first_or_default_col(0);
397397
for a in 0..n {
398-
assert_eq!(col.get_val(a as u32), permutation[a]);
398+
assert_eq!(col.get_val(a as u32), permutation[a], "for doc {a}");
399399
}
400400
}
401401

0 commit comments

Comments
 (0)