Skip to content

Commit 75dcf78

Browse files
authored
Assertions in functions are more optimisation friendly (#195)
Most of the asserts in this codebase cause the compiler to capture arguments. We can get 99% of the assert functionality by removing the formatting from messages and letting the default message handle it. While doing this I realised that our unpack_single bounds are not obvious to the compiler and it can't optimise away bounds check at the final value access. This is also fixed here to make these functions more optimisation friendly
1 parent 325a8b7 commit 75dcf78

3 files changed

Lines changed: 51 additions & 14 deletions

File tree

src/bitpacking.rs

Lines changed: 45 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,12 @@ pub trait BitPacking: FastLanes {
2020
/// - The input slice must be of exactly length 1024.
2121
/// - The output slice must be of length `1024 * W / T`, where `T` is the (unpacked) bit-width
2222
/// of `Self` and `W` is the packed bit-width.
23-
/// - The `width` must be less than or equal to the (unpacked) bit-width of `Self`.
2423
///
2524
/// These lengths are checked only with `debug_assert` (i.e., not checked on release builds).
25+
///
26+
/// # Panics
27+
///
28+
/// Panics if `width` is greater than the (unpacked) bit-width of `Self`.
2629
unsafe fn unchecked_pack(width: usize, input: &[Self], output: &mut [Self]);
2730

2831
/// Unpacks 1024 elements from `W` bits each.
@@ -36,12 +39,19 @@ pub trait BitPacking: FastLanes {
3639
/// - The input slice must be of length `1024 * W / T`, where `T` is the (unpacked) bit-width
3740
/// of `Self` and `W` is the packed bit-width.
3841
/// - The output slice must be of exactly length 1024.
39-
/// - The `width` must be less than or equal to the (unpacked) bit-width of `Self`.
4042
///
4143
/// These lengths are checked only with `debug_assert` (i.e., not checked on release builds).
44+
///
45+
/// # Panics
46+
///
47+
/// Panics if `width` is greater than the (unpacked) bit-width of `Self`.
4248
unsafe fn unchecked_unpack(width: usize, input: &[Self], output: &mut [Self]);
4349

4450
/// Unpacks a single element at the provided index from a packed array of 1024 `W` bit elements.
51+
///
52+
/// # Panics
53+
///
54+
/// Panics if `W` is not zero and `index` is not less than 1024.
4555
fn unpack_single<const W: usize, const B: usize>(packed: &[Self; B], index: usize) -> Self;
4656

4757
/// Unpacks a single element at the provided index from a packed array of 1024 `W` bit elements,
@@ -51,9 +61,13 @@ pub trait BitPacking: FastLanes {
5161
///
5262
/// - The input slice must be of length `1024 * W / T`, where `T` is the (unpacked) bit-width
5363
/// of `Self` and `W` is the packed bit-width.
54-
/// - The `width` must be less than or equal to the (unpacked) bit-width of `Self`.
5564
///
56-
/// These lengths are checked only with `debug_assert` (i.e., not checked on release builds).
65+
/// This length is checked only with `debug_assert` (i.e., not checked on release builds).
66+
///
67+
/// # Panics
68+
///
69+
/// Panics if `width` is greater than the (unpacked) bit-width of `Self` or, when `width` is
70+
/// not zero, `index` is not less than 1024.
5771
unsafe fn unchecked_unpack_single(width: usize, input: &[Self], index: usize) -> Self;
5872

5973
/// Unpacks selected elements from a packed array of 1024 `W` bit elements.
@@ -76,14 +90,13 @@ pub trait BitPacking: FastLanes {
7690
///
7791
/// - The input slice must contain exactly `1024 * W / T` elements, where `T` is the unpacked bit
7892
/// width of `Self` and `W` is `width`.
79-
/// - The `width` must be less than or equal to the unpacked bit width of `Self`.
8093
///
81-
/// These conditions are checked only with `debug_assert`.
94+
/// This length is checked only with `debug_assert` (i.e., not checked on release builds).
8295
///
8396
/// # Panics
8497
///
85-
/// Panics if the output length differs from the index length or, when `width` is not zero, an
86-
/// index is not less than 1024.
98+
/// Panics if `width` is greater than the unpacked bit width of `Self`, if the output length
99+
/// differs from the index length or, when `width` is not zero, an index is not less than 1024.
87100
unsafe fn unchecked_unpack_indices(
88101
width: usize,
89102
input: &[Self],
@@ -215,16 +228,25 @@ macro_rules! impl_packing {
215228
// decompression can be fused efficiently with encodings like delta and RLE.
216229
//
217230
// First step, we need to get the lane and row for interpretation #1 above.
218-
assert!(index < 1024, "Index must be less than 1024, got {}", index);
231+
// Indexing the 1024-entry tables is the `index < 1024` bounds check; a separate
232+
// assert would only add a second panic path that captures `index` for formatting.
219233
let (lane, row): (usize, usize) = {
220234
const LANES: [u8; 1024] = lanes_by_index::<$T>();
221235
const ROWS: [u8; 1024] = rows_by_index::<$T>();
222236
(LANES[index] as usize, ROWS[index] as usize)
223237
};
224238

239+
// The table lookups above bound `lane < LANES` and `row < T`, and the `const`
240+
// block bounds `B == LANES * W`. Every `packed` read below is therefore in-bounds
241+
// by construction, so it is `get_unchecked` rather than a checked index whose
242+
// bounds LLVM cannot prove away through the table loads.
243+
225244
if W == <$T>::T {
226-
// Special case for W==T, we can just read the value directly
227-
return packed[<$T>::LANES * row + lane];
245+
// Special case for W==T, we can just read the value directly.
246+
// SAFETY: `LANES * row + lane <= LANES * (T - 1) + LANES - 1 = 1024 - 1 < B`.
247+
let word = <$T>::LANES * row + lane;
248+
debug_assert!(word < B);
249+
return unsafe { *packed.get_unchecked(word) };
228250
}
229251

230252
let mask: $T = (1 << (W % <$T>::T)) - 1;
@@ -233,13 +255,22 @@ macro_rules! impl_packing {
233255
let lo_shift = start_bit % <$T>::T;
234256
let remaining_bits = <$T>::T - lo_shift;
235257

236-
let lo = packed[<$T>::LANES * start_word + lane] >> lo_shift;
258+
// SAFETY: `start_word = row * W / T <= (T - 1) * W / T < W`, so
259+
// `LANES * start_word + lane < LANES * W == B`.
260+
let lo_word = <$T>::LANES * start_word + lane;
261+
debug_assert!(lo_word < B);
262+
let lo = unsafe { *packed.get_unchecked(lo_word) } >> lo_shift;
237263
return if remaining_bits >= W {
238264
// in this case we will mask out all bits of hi word
239265
lo & mask
240266
} else {
241267
// guaranteed that lo_shift > 0 and thus remaining_bits < T
242-
let hi = packed[<$T>::LANES * (start_word + 1) + lane] << remaining_bits;
268+
// SAFETY: the element straddles `start_word` and `start_word + 1`, so its last
269+
// bit `row * W + W - 1 <= T * W - 1` lies in word `start_word + 1 <= W - 1`,
270+
// hence `LANES * (start_word + 1) + lane < LANES * W == B`.
271+
let hi_word = <$T>::LANES * (start_word + 1) + lane;
272+
debug_assert!(hi_word < B);
273+
let hi = unsafe { *packed.get_unchecked(hi_word) } << remaining_bits;
243274
(lo | hi) & mask
244275
};
245276
}
@@ -285,7 +316,7 @@ macro_rules! impl_packing {
285316
assert!(B == 1024 * W / Self::T);
286317
}
287318

288-
assert_eq!(indices.len(), output.len(), "Output length must equal index length");
319+
assert!(indices.len() == output.len(), "Output length must equal index length");
289320
if W == 0 {
290321
for value in output {
291322
value.write(0 as Self);

src/bitpacking_cmp.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,9 @@ pub trait BitPackingCompare: FastLanes {
4040
/// The input slice must be of length `1024 * W / T`, where `T` is the bit-width of Self and `W`
4141
/// is the packed width. The output is exactly `[u64; 16]` (`1024` bits).
4242
/// These lengths are checked only with `debug_assert` (i.e., not checked on release builds).
43+
///
44+
/// # Panics
45+
/// Panics if `width` is greater than the bit-width of `Self`.
4346
unsafe fn unchecked_unpack_cmp<V, F>(
4447
width: usize,
4548
input: &[Self],

src/ffor.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,9 @@ pub trait FoR: BitPacking {
2222
/// The input slice must be of length `1024 * W / T`, where `T` is the bit-width of Self and `W`
2323
/// is the packed width. The output slice must be of exactly length 1024.
2424
/// These lengths are checked only with `debug_assert` (i.e., not checked on release builds).
25+
///
26+
/// # Panics
27+
/// Panics if `width` is greater than the bit-width of `Self`.
2528
unsafe fn unchecked_unfor_pack(
2629
width: usize,
2730
input: &[Self],

0 commit comments

Comments
 (0)