Skip to content

Commit e4bc02a

Browse files
Add structured decoding (#79)
* Add DecodedStore for random-access decoding, complete from_store coverage DecodedStore is a zero-allocation view into Indexed-encoded data that provides O(1) random access to individual slices. from_store on FromBytes uses it to give each field independent access at its compile-time-known offset, eliminating the iterator dependency chain. Assembly: accessing any field of any tuple width is now 49-54 instructions on the critical path, constant in both k (tuple width) and field position. LLVM fully eliminates unused fields across all type combinators: tuples, Results, Options, Vecs, Strings, and arbitrary nesting. Also adds from_u64s and from_store overrides to all remaining FromBytes impls (Usizes, Isizes, Chars, U128s, I128s, Fixeds, Strides, Empties, Bools, Durations) to ensure no leaf type breaks the optimization chain. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Remove from_u64s, decode_u64s, and from_byte_slices These are superseded by from_store + DecodedStore which provides random access without iterator overhead. The FromBytes trait now has three methods beyond from_bytes: from_store, element_sizes, and validate. The derive macro generates from_store automatically. Also fixes references to the removed Indexed struct in doc comments. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 8f254ac commit e4bc02a

File tree

13 files changed

+206
-261
lines changed

13 files changed

+206
-261
lines changed

columnar_derive/src/lib.rs

Lines changed: 11 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -323,17 +323,8 @@ fn derive_struct(name: &syn::Ident, generics: &syn::Generics, data_struct: syn::
323323
Self { #(#names: ::columnar::FromBytes::from_bytes(bytes),)* }
324324
}
325325
#[inline(always)]
326-
fn from_byte_slices(bytes: &[&'columnar [u8]]) -> Self {
327-
let mut _offset = 0;
328-
#(
329-
let #names = <#container_types>::from_byte_slices(&bytes[_offset .. _offset + <#container_types>::SLICE_COUNT]);
330-
_offset += <#container_types>::SLICE_COUNT;
331-
)*
332-
Self { #(#names,)* }
333-
}
334-
#[inline(always)]
335-
fn from_u64s(words: &mut impl Iterator<Item=(&'columnar [u64], u8)>) -> Self {
336-
Self { #(#names: ::columnar::FromBytes::from_u64s(words),)* }
326+
fn from_store(store: &::columnar::bytes::indexed::DecodedStore<'columnar>, offset: &mut usize) -> Self {
327+
Self { #(#names: ::columnar::FromBytes::from_store(store, offset),)* }
337328
}
338329
}
339330
}
@@ -520,13 +511,10 @@ fn derive_unit_struct(name: &syn::Ident, _generics: &syn::Generics, vis: syn::Vi
520511
Self { count: &::columnar::bytemuck::try_cast_slice(bytes.next().unwrap()).unwrap()[0] }
521512
}
522513
#[inline(always)]
523-
fn from_byte_slices(bytes: &[&'columnar [u8]]) -> Self {
524-
Self { count: &::columnar::bytemuck::try_cast_slice(bytes[0]).unwrap()[0] }
525-
}
526-
#[inline(always)]
527-
fn from_u64s(words: &mut impl Iterator<Item=(&'columnar [u64], u8)>) -> Self {
528-
let (w, _tail) = words.next().expect("Iterator exhausted prematurely");
529-
Self { count: &w[0] }
514+
fn from_store(store: &::columnar::bytes::indexed::DecodedStore<'columnar>, offset: &mut usize) -> Self {
515+
let (w, _tail) = store.get(*offset);
516+
*offset += 1;
517+
Self { count: w.first().unwrap_or(&0) }
530518
}
531519
}
532520

@@ -910,20 +898,10 @@ fn derive_enum(name: &syn::Ident, generics: &syn:: Generics, data_enum: syn::Dat
910898
}
911899
}
912900
#[inline(always)]
913-
fn from_byte_slices(bytes: &[&'columnar [u8]]) -> Self {
914-
let mut _offset = 0;
915-
#(
916-
let #names = <#container_types>::from_byte_slices(&bytes[_offset .. _offset + <#container_types>::SLICE_COUNT]);
917-
_offset += <#container_types>::SLICE_COUNT;
918-
)*
919-
let indexes = <::columnar::Discriminant<CVar, COff, CC>>::from_byte_slices(&bytes[_offset ..]);
920-
Self { #(#names,)* indexes }
921-
}
922-
#[inline(always)]
923-
fn from_u64s(words: &mut impl Iterator<Item=(&'columnar [u64], u8)>) -> Self {
901+
fn from_store(store: &::columnar::bytes::indexed::DecodedStore<'columnar>, offset: &mut usize) -> Self {
924902
Self {
925-
#(#names: ::columnar::FromBytes::from_u64s(words),)*
926-
indexes: ::columnar::FromBytes::from_u64s(words),
903+
#(#names: ::columnar::FromBytes::from_store(store, offset),)*
904+
indexes: ::columnar::FromBytes::from_store(store, offset),
927905
}
928906
}
929907
}
@@ -1216,12 +1194,8 @@ fn derive_tags(name: &syn::Ident, _generics: &syn:: Generics, data_enum: syn::Da
12161194
Self { variant: ::columnar::FromBytes::from_bytes(bytes) }
12171195
}
12181196
#[inline(always)]
1219-
fn from_byte_slices(bytes: &[&'columnar [u8]]) -> Self {
1220-
Self { variant: CVar::from_byte_slices(bytes) }
1221-
}
1222-
#[inline(always)]
1223-
fn from_u64s(words: &mut impl Iterator<Item=(&'columnar [u64], u8)>) -> Self {
1224-
Self { variant: ::columnar::FromBytes::from_u64s(words) }
1197+
fn from_store(store: &::columnar::bytes::indexed::DecodedStore<'columnar>, offset: &mut usize) -> Self {
1198+
Self { variant: ::columnar::FromBytes::from_store(store, offset) }
12251199
}
12261200
}
12271201

examples/decode_asm.rs

Lines changed: 31 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,10 @@
11
//! Assembly inspection for decode paths.
22
//!
3-
//! Compares three approaches to accessing a single field of a k-tuple
4-
//! stored in Indexed-encoded `&[u64]` data:
3+
//! Compares two approaches to accessing a single field of a k-tuple
4+
//! stored in indexed-encoded `&[u64]` data:
55
//!
66
//! 1. `from_bytes` + `decode`: constructs all k fields, O(k)
7-
//! 2. `from_u64s` + `decode_u64s`: non-panicking, LLVM eliminates unused fields, O(1) in k
8-
//! 3. `decode_field` (random access): decodes one field directly, O(1) in k and j
7+
//! 2. `from_store` + `DecodedStore`: random access, LLVM eliminates unused fields, O(1)
98
//!
109
//! Build with: `cargo rustc --example decode_asm --release -- --emit asm`
1110
@@ -36,64 +35,56 @@ use columnar::bytes::indexed;
3635
}
3736

3837
// ================================================================
39-
// from_u64s path (non-panicking, LLVM eliminates unused fields)
38+
// DecodedStore path (random access, O(1) in both k and field position)
4039
// ================================================================
4140

42-
#[no_mangle] pub fn u64s_3_f0(store: &[u64], i: usize) -> u64 {
41+
#[no_mangle] pub fn store_3_f0(store: &[u64], i: usize) -> u64 {
4342
type T<'a> = (&'a [u64], &'a [u64], &'a [u64]);
44-
T::from_u64s(&mut indexed::decode_u64s(store)).0[i]
43+
let ds = indexed::DecodedStore::new(store);
44+
T::from_store(&ds, &mut 0).0[i]
4545
}
46-
#[no_mangle] pub fn u64s_3_flast(store: &[u64], i: usize) -> u64 {
46+
#[no_mangle] pub fn store_3_flast(store: &[u64], i: usize) -> u64 {
4747
type T<'a> = (&'a [u64], &'a [u64], &'a [u64]);
48-
T::from_u64s(&mut indexed::decode_u64s(store)).2[i]
48+
let ds = indexed::DecodedStore::new(store);
49+
T::from_store(&ds, &mut 0).2[i]
4950
}
50-
#[no_mangle] pub fn u64s_8_f0(store: &[u64], i: usize) -> u64 {
51+
#[no_mangle] pub fn store_8_f0(store: &[u64], i: usize) -> u64 {
5152
type T<'a> = (&'a [u64], &'a [u64], &'a [u64], &'a [u64],
5253
&'a [u64], &'a [u64], &'a [u64], &'a [u64]);
53-
T::from_u64s(&mut indexed::decode_u64s(store)).0[i]
54+
let ds = indexed::DecodedStore::new(store);
55+
T::from_store(&ds, &mut 0).0[i]
5456
}
55-
#[no_mangle] pub fn u64s_8_flast(store: &[u64], i: usize) -> u64 {
57+
#[no_mangle] pub fn store_8_flast(store: &[u64], i: usize) -> u64 {
5658
type T<'a> = (&'a [u64], &'a [u64], &'a [u64], &'a [u64],
5759
&'a [u64], &'a [u64], &'a [u64], &'a [u64]);
58-
T::from_u64s(&mut indexed::decode_u64s(store)).7[i]
60+
let ds = indexed::DecodedStore::new(store);
61+
T::from_store(&ds, &mut 0).7[i]
5962
}
6063

6164
// ================================================================
62-
// Random access (decode one field directly, O(1) in both k and j)
65+
// Complex types: do unused complex fields get eliminated?
6366
// ================================================================
6467

65-
/// Decode field `k` directly from store as `(&[u64], u8)`.
66-
/// Each call is independent — no iterator state.
67-
#[inline(always)]
68-
fn decode_field(store: &[u64], k: usize) -> (&[u64], u8) {
69-
let slices = store[0] as usize / 8 - 1;
70-
let index = &store[..slices + 1];
71-
let last = *index.last().unwrap_or(&0) as usize;
72-
let last_w = (last + 7) / 8;
73-
let words = &store[..last_w];
74-
let upper = (*index.get(k + 1).unwrap_or(&0) as usize).min(last);
75-
let lower = (((*index.get(k).unwrap_or(&0) as usize) + 7) & !7).min(upper);
76-
let upper_w = ((upper + 7) / 8).min(words.len());
77-
let lower_w = (lower / 8).min(upper_w);
78-
let tail = (upper % 8) as u8;
79-
(&words[lower_w..upper_w], tail)
68+
#[no_mangle] pub fn store_u64_result_f0(store: &[u64], i: usize) -> u64 {
69+
type T<'a> = (&'a [u64], columnar::Results<&'a [u64], &'a [u64], &'a [u64], &'a [u64], &'a u64>);
70+
let ds = indexed::DecodedStore::new(store);
71+
*T::from_store(&ds, &mut 0).0.get(i).unwrap()
8072
}
8173

82-
#[no_mangle] pub fn field_3_f0(store: &[u64], i: usize) -> u64 {
83-
decode_field(store, 0).0[i]
74+
#[no_mangle] pub fn store_u64_string_vec_f0(store: &[u64], i: usize) -> u64 {
75+
type T<'a> = (&'a [u64], columnar::Strings<&'a [u64], &'a [u8]>, columnar::Vecs<&'a [u32], &'a [u64]>);
76+
let ds = indexed::DecodedStore::new(store);
77+
T::from_store(&ds, &mut 0).0[i]
8478
}
85-
#[no_mangle] pub fn field_3_flast(store: &[u64], i: usize) -> u64 {
86-
decode_field(store, 2).0[i]
87-
}
88-
#[no_mangle] pub fn field_8_f0(store: &[u64], i: usize) -> u64 {
89-
decode_field(store, 0).0[i]
90-
}
91-
#[no_mangle] pub fn field_8_flast(store: &[u64], i: usize) -> u64 {
92-
decode_field(store, 7).0[i]
79+
80+
#[no_mangle] pub fn store_nested_inner(store: &[u64], i: usize) -> u64 {
81+
type T<'a> = (&'a [u64], (&'a [u64], (&'a [u64], &'a [u64])));
82+
let ds = indexed::DecodedStore::new(store);
83+
T::from_store(&ds, &mut 0).1.1.1[i]
9384
}
9485

9586
fn main() {
9687
let mut store = vec![0u64; 100];
9788
store[0] = 32; store[1] = 32; store[2] = 32; store[3] = 32;
98-
println!("{}", std::hint::black_box(field_3_f0(std::hint::black_box(&store), 0)));
89+
println!("{}", std::hint::black_box(store_3_f0(std::hint::black_box(&store), 0)));
9990
}

examples/decode_bench.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
//! Benchmarks for Indexed::decode improvements.
1+
//! Benchmarks for indexed decode improvements.
22
//!
33
//! Measures decode + field access from encoded `[u64]` data,
44
//! exercising both simple and complex types, and separating
@@ -21,7 +21,7 @@ fn bench_ns<F: FnMut()>(iters: u64, mut f: F) -> f64 {
2121
elapsed.as_nanos() as f64 / iters as f64
2222
}
2323

24-
/// Encode a container into Indexed format, returning the `[u64]` store.
24+
/// Encode a container into indexed format, returning the `[u64]` store.
2525
fn encode_indexed<C: Borrow>(container: &C) -> Vec<u64>
2626
where
2727
for<'a> C::Borrowed<'a>: AsBytes<'a>,

src/arc.rs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,7 @@ impl<'a, T: AsBytes<'a>> AsBytes<'a> for Arc<T> {
2525
impl<'a, T: FromBytes<'a>> FromBytes<'a> for Arc<T> {
2626
const SLICE_COUNT: usize = T::SLICE_COUNT;
2727
#[inline(always)] fn from_bytes(bytes: &mut impl Iterator<Item=&'a [u8]>) -> Self { Arc::new(T::from_bytes(bytes)) }
28-
#[inline(always)] fn from_byte_slices(bytes: &[&'a [u8]]) -> Self { Arc::new(T::from_byte_slices(bytes)) }
29-
#[inline(always)] fn from_u64s(words: &mut impl Iterator<Item=(&'a [u64], u8)>) -> Self { Arc::new(T::from_u64s(words)) }
28+
#[inline(always)] fn from_store(store: &crate::bytes::indexed::DecodedStore<'a>, offset: &mut usize) -> Self { Arc::new(T::from_store(store, offset)) }
3029
}
3130

3231
#[cfg(test)]

src/boxed.rs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -61,8 +61,7 @@ impl<'a, C: AsBytes<'a>> AsBytes<'a> for Boxed<C> {
6161
impl<'a, C: FromBytes<'a>> FromBytes<'a> for Boxed<C> {
6262
const SLICE_COUNT: usize = C::SLICE_COUNT;
6363
#[inline(always)] fn from_bytes(bytes: &mut impl Iterator<Item=&'a [u8]>) -> Self { Self(C::from_bytes(bytes)) }
64-
#[inline(always)] fn from_byte_slices(bytes: &[&'a [u8]]) -> Self { Self(C::from_byte_slices(bytes)) }
65-
#[inline(always)] fn from_u64s(words: &mut impl Iterator<Item=(&'a [u64], u8)>) -> Self { Self(C::from_u64s(words)) }
64+
#[inline(always)] fn from_store(store: &crate::bytes::indexed::DecodedStore<'a>, offset: &mut usize) -> Self { Self(C::from_store(store, offset)) }
6665
}
6766
impl<C: Index> Index for Boxed<C> {
6867
type Ref = Boxed<C::Ref>;

0 commit comments

Comments
 (0)