@@ -38,14 +38,13 @@ pub trait LayoutChildren: 'static + Send + Sync {
3838
3939 fn nchildren ( & self ) -> usize ;
4040
41- /// Returns `true` if the child at `idx` is known — without materializing it — to register no
42- /// split boundaries strictly inside its row range (see
43- /// [`VTable::registers_interior_splits`](crate::VTable::registers_interior_splits)).
41+ /// Returns `true` if the child at `idx` is divisible: it may register split boundaries
42+ /// strictly inside its row range (see [`VTable::is_divisible`](crate::VTable::is_divisible)).
4443 ///
45- /// Implementations may conservatively return `false ` when the answer would require
44+ /// Implementations must conservatively return `true ` when answering would require
4645 /// materializing the child.
47- fn child_divisibility ( & self , _idx : usize ) -> bool {
48- false
46+ fn child_is_divisible ( & self , _idx : usize ) -> bool {
47+ true
4948 }
5049}
5150
@@ -74,8 +73,8 @@ impl LayoutChildren for Arc<dyn LayoutChildren> {
7473 self . as_ref ( ) . nchildren ( )
7574 }
7675
77- fn child_divisibility ( & self , idx : usize ) -> bool {
78- self . as_ref ( ) . child_divisibility ( idx)
76+ fn child_is_divisible ( & self , idx : usize ) -> bool {
77+ self . as_ref ( ) . child_is_divisible ( idx)
7978 }
8079}
8180
@@ -119,8 +118,8 @@ impl LayoutChildren for OwnedLayoutChildren {
119118 self . 0 . len ( )
120119 }
121120
122- fn child_divisibility ( & self , idx : usize ) -> bool {
123- ! self . 0 [ idx] . dyn_registers_interior_splits ( )
121+ fn child_is_divisible ( & self , idx : usize ) -> bool {
122+ self . 0 [ idx] . dyn_is_divisible ( )
124123 }
125124}
126125
@@ -134,10 +133,7 @@ pub(crate) struct ViewedLayoutChildren {
134133 allow_unknown : bool ,
135134 session : VortexSession ,
136135 cache : Arc < [ OnceCell < LayoutRef > ] > ,
137- /// Single-slot memo for [`LayoutChildren::child_has_no_interior_splits`]: children of one
138- /// layout node almost always share an encoding, so remember the last resolved flatbuffer
139- /// encoding tag and its answer. Packed as `tag | (answer << 16) | (valid << 17)`.
140- no_interior_splits_memo : Arc < AtomicU32 > ,
136+ divisibility_memo : DivisibilityMemo ,
141137}
142138
143139impl ViewedLayoutChildren {
@@ -170,7 +166,7 @@ impl ViewedLayoutChildren {
170166 allow_unknown,
171167 session,
172168 cache,
173- no_interior_splits_memo : Arc :: new ( AtomicU32 :: new ( 0 ) ) ,
169+ divisibility_memo : DivisibilityMemo :: empty ( ) ,
174170 }
175171 }
176172
@@ -296,37 +292,85 @@ impl LayoutChildren for ViewedLayoutChildren {
296292 self . cache . len ( )
297293 }
298294
299- fn child_divisibility ( & self , idx : usize ) -> bool {
295+ fn child_is_divisible ( & self , idx : usize ) -> bool {
300296 if idx >= self . nchildren ( ) {
301- return false ;
297+ return true ;
302298 }
303299 // Resolve the child's layout encoding from the flatbuffer tag alone, without
304- // deserializing the child layout. Unknown encodings conservatively report interior
305- // splits so callers fall back to materializing the child.
300+ // deserializing the child layout. Unknown encodings are conservatively divisible so
301+ // callers fall back to materializing the child.
306302 let encoding = self
307303 . flatbuffer ( )
308304 . children ( )
309305 . unwrap_or_default ( )
310306 . get ( idx)
311307 . encoding ( ) ;
312308
313- const MEMO_VALID : u32 = 1 << 17 ;
314- const MEMO_ANSWER : u32 = 1 << 16 ;
315- const MEMO_TAG : u32 = 0xFFFF ;
316- let memo = self . no_interior_splits_memo . load ( Ordering :: Relaxed ) ;
317- if memo & MEMO_VALID != 0 && memo & MEMO_TAG == u32:: from ( encoding) {
318- return memo & MEMO_ANSWER != 0 ;
309+ if let Some ( answer) = self . divisibility_memo . get ( encoding) {
310+ return answer;
319311 }
320312
321313 let answer = self
322314 . layout_read_ctx
323315 . resolve ( encoding)
324316 . and_then ( |encoding_id| self . layouts . get ( & encoding_id) )
325- . is_some_and ( |encoding| !encoding. registers_interior_splits ( ) ) ;
326- self . no_interior_splits_memo . store (
327- u32:: from ( encoding) | MEMO_VALID | if answer { MEMO_ANSWER } else { 0 } ,
317+ . is_none_or ( |encoding| encoding. is_divisible ( ) ) ;
318+ self . divisibility_memo . set ( encoding, answer) ;
319+ answer
320+ }
321+ }
322+
323+ /// Single-slot cache for [`ViewedLayoutChildren::child_is_divisible`] answers, keyed by the
324+ /// child's flatbuffer encoding tag and shared across clones of the owning
325+ /// [`ViewedLayoutChildren`].
326+ ///
327+ /// Divisibility depends only on the child's layout encoding, and children of one layout node
328+ /// almost always share an encoding — so caching the answer for the last-seen tag turns the
329+ /// per-child read-context resolve and registry lookup into a single atomic load for all but the
330+ /// first child.
331+ ///
332+ /// The tag, the answer, and a validity flag are packed into one `AtomicU32` so lookups and
333+ /// updates are each a single atomic operation, with no locking and no torn state between the key
334+ /// and its answer:
335+ ///
336+ /// ```text
337+ /// bit: 17 16 15..0
338+ /// valid | answer | encoding tag
339+ /// ```
340+ ///
341+ /// `Relaxed` ordering suffices throughout: this is purely a performance cache, and each packed
342+ /// word is internally consistent on its own. The worst a racing `get`/`set` can cause is a
343+ /// redundant recomputation.
344+ #[ derive( Clone ) ]
345+ struct DivisibilityMemo ( Arc < AtomicU32 > ) ;
346+
347+ impl DivisibilityMemo {
348+ /// Low 16 bits: the flatbuffer encoding tag the cached answer belongs to.
349+ const TAG : u32 = 0xFFFF ;
350+ /// Bit 16: the cached answer for the stored tag.
351+ const ANSWER : u32 = 1 << 16 ;
352+ /// Bit 17: set once the memo holds an entry. Needed because the atomic starts at zero, which
353+ /// would otherwise be indistinguishable from a cached `(tag 0, answer false)` entry.
354+ const VALID : u32 = 1 << 17 ;
355+
356+ /// Create a memo holding no entry.
357+ fn empty ( ) -> Self {
358+ Self ( Arc :: new ( AtomicU32 :: new ( 0 ) ) )
359+ }
360+
361+ /// Return the cached answer for `tag`, or `None` if the memo is empty or holds a different
362+ /// tag.
363+ fn get ( & self , tag : u16 ) -> Option < bool > {
364+ let memo = self . 0 . load ( Ordering :: Relaxed ) ;
365+ ( memo & Self :: VALID != 0 && memo & Self :: TAG == u32:: from ( tag) )
366+ . then_some ( memo & Self :: ANSWER != 0 )
367+ }
368+
369+ /// Cache `answer` for `tag`, replacing any previous entry.
370+ fn set ( & self , tag : u16 , answer : bool ) {
371+ self . 0 . store (
372+ u32:: from ( tag) | Self :: VALID | if answer { Self :: ANSWER } else { 0 } ,
328373 Ordering :: Relaxed ,
329374 ) ;
330- answer
331375 }
332376}
0 commit comments