@@ -35,6 +35,7 @@ use differential_dataflow::lattice::Lattice;
3535use differential_dataflow:: logging:: Logger ;
3636use differential_dataflow:: trace:: implementations:: merge_batcher:: MergeBatcher ;
3737use differential_dataflow:: trace:: { Batcher , Description } ;
38+ use mz_ore:: cast:: CastFrom ;
3839use mz_repr:: Row ;
3940use mz_timely_util:: columnar:: Column ;
4041use mz_timely_util:: columnation:: { ColInternalMerger , ColumnationStack } ;
@@ -153,40 +154,61 @@ where
153154 let get = |e : & ( u64 , u32 , u32 ) | {
154155 & pending[ usize:: try_from ( e. 1 ) . unwrap ( ) ] [ usize:: try_from ( e. 2 ) . unwrap ( ) ]
155156 } ;
156- // Full comparisons only on equal prefixes, which `sort_prefix` guarantees agree with the
157- // row order otherwise.
158- index. sort_unstable_by ( |a, b| {
159- a. 0 . cmp ( & b. 0 ) . then_with ( || {
160- let ( da, ta, _) = get ( a) ;
161- let ( db, tb, _) = get ( b) ;
162- ( da, ta) . cmp ( & ( db, tb) )
163- } )
164- } ) ;
157+ // Sort by prefix alone; equal prefixes are settled by full comparison as they are gathered,
158+ // which `sort_prefix` guarantees agrees with the row order otherwise.
159+ radix_sort_by_prefix ( & mut index) ;
165160
166161 let cap = Self :: chunk_capacity ( ) ;
167162 let mut output = Vec :: with_capacity ( self . pending_len / cap + 1 ) ;
168163 let mut result: Chunk < D , T , R > = ColumnationStack :: with_capacity ( cap) ;
169- let mut iter = index. iter ( ) . peekable ( ) ;
170- while let Some ( e) = iter. next ( ) {
171- let ( d, t, r) = get ( e) ;
172- let mut diff = r. clone ( ) ;
173- while let Some ( n) = iter. peek ( ) {
174- let ( d2, t2, r2) = get ( n) ;
175- if ( d2, t2) . cmp ( & ( d, t) ) == Ordering :: Equal {
176- diff. plus_equals ( r2) ;
177- iter. next ( ) ;
178- } else {
179- break ;
180- }
164+ // The sorted order visits the held rows at random, so each row is a cache miss. Touching
165+ // the row `LOOKAHEAD` entries ahead lets that miss overlap the work on this row. The
166+ // value read is unused.
167+ const LOOKAHEAD : usize = 16 ;
168+ let n = index. len ( ) ;
169+ let mut i = 0 ;
170+ while i < n {
171+ if i + LOOKAHEAD < n {
172+ let ( d, _, _) = get ( & index[ i + LOOKAHEAD ] ) ;
173+ // SAFETY: `d` is a live element of a held chunk.
174+ unsafe { std:: ptr:: read_volatile ( std:: ptr:: from_ref ( d) . cast :: < u8 > ( ) ) } ;
181175 }
182- if !diff. is_zero ( ) {
183- if result. len ( ) == cap {
184- output. push ( std:: mem:: replace (
185- & mut result,
186- ColumnationStack :: with_capacity ( cap) ,
187- ) ) ;
176+ // A run of equal prefixes is the only place rows can compare equal, and the only
177+ // place the prefix order leaves undecided. Order the run by full comparison first.
178+ let prefix = index[ i] . 0 ;
179+ let mut end = i + 1 ;
180+ while end < n && index[ end] . 0 == prefix {
181+ end += 1 ;
182+ }
183+ if end - i > 1 {
184+ index[ i..end] . sort_unstable_by ( |a, b| {
185+ let ( da, ta, _) = get ( a) ;
186+ let ( db, tb, _) = get ( b) ;
187+ ( da, ta) . cmp ( & ( db, tb) )
188+ } ) ;
189+ }
190+ while i < end {
191+ let ( d, t, r) = get ( & index[ i] ) ;
192+ let mut diff = r. clone ( ) ;
193+ i += 1 ;
194+ while i < end {
195+ let ( d2, t2, r2) = get ( & index[ i] ) ;
196+ if ( d2, t2) . cmp ( & ( d, t) ) == Ordering :: Equal {
197+ diff. plus_equals ( r2) ;
198+ i += 1 ;
199+ } else {
200+ break ;
201+ }
202+ }
203+ if !diff. is_zero ( ) {
204+ if result. len ( ) == cap {
205+ output. push ( std:: mem:: replace (
206+ & mut result,
207+ ColumnationStack :: with_capacity ( cap) ,
208+ ) ) ;
209+ }
210+ result. copy_destructured ( d, t, & diff) ;
188211 }
189- result. copy_destructured ( d, t, & diff) ;
190212 }
191213 }
192214 if !result. is_empty ( ) {
@@ -198,6 +220,55 @@ where
198220 }
199221}
200222
223+ /// Sorts `index` by its `u64` prefix with a least-significant-digit radix sort, skipping the
224+ /// byte positions on which every prefix agrees.
225+ ///
226+ /// Row prefixes share their length and leading tag bytes, so typically three or four of the
227+ /// eight passes run. Each pass streams the index once, against the log-factor of a comparison
228+ /// sort over a 16-byte-entry index that no longer fits in cache. Small inputs use the
229+ /// comparison sort, whose constant is lower.
230+ fn radix_sort_by_prefix ( index : & mut Vec < ( u64 , u32 , u32 ) > ) {
231+ const RADIX_MIN : usize = 1 << 16 ;
232+ let n = index. len ( ) ;
233+ if n < RADIX_MIN {
234+ index. sort_unstable_by_key ( |e| e. 0 ) ;
235+ return ;
236+ }
237+ let mut histograms = [ [ 0u32 ; 256 ] ; 8 ] ;
238+ for ( prefix, _, _) in index. iter ( ) {
239+ for ( digit, histogram) in histograms. iter_mut ( ) . enumerate ( ) {
240+ histogram[ usize:: cast_from ( ( prefix >> ( 8 * digit) ) & 0xFF ) ] += 1 ;
241+ }
242+ }
243+ let mut scratch: Vec < ( u64 , u32 , u32 ) > = vec ! [ ( 0 , 0 , 0 ) ; n] ;
244+ let mut in_index = true ;
245+ for ( digit, histogram) in histograms. iter ( ) . enumerate ( ) {
246+ if histogram. iter ( ) . any ( |& count| usize:: cast_from ( count) == n) {
247+ continue ;
248+ }
249+ let mut offsets = [ 0usize ; 256 ] ;
250+ let mut sum = 0 ;
251+ for ( bucket, & count) in histogram. iter ( ) . enumerate ( ) {
252+ offsets[ bucket] = sum;
253+ sum += usize:: cast_from ( count) ;
254+ }
255+ let ( src, dst) = if in_index {
256+ ( & * index, & mut scratch)
257+ } else {
258+ ( & scratch, & mut * index)
259+ } ;
260+ for entry in src. iter ( ) {
261+ let bucket = usize:: cast_from ( ( entry. 0 >> ( 8 * digit) ) & 0xFF ) ;
262+ dst[ offsets[ bucket] ] = * entry;
263+ offsets[ bucket] += 1 ;
264+ }
265+ in_index = !in_index;
266+ }
267+ if !in_index {
268+ std:: mem:: swap ( index, & mut scratch) ;
269+ }
270+ }
271+
201272impl < D , T , R > Batcher for SnapshotBatcher < D , T , R >
202273where
203274 D : SortPrefix + Ord + Columnation + Clone + ' static ,
@@ -542,6 +613,136 @@ mod tests {
542613 assert_eq ! ( keys, vec![ 4 , 3 , 2 , 1 , 0 ] ) ;
543614 }
544615
616+ /// Timing of the snapshot path on exchange-shaped input. Run with
617+ /// `cargo test --profile optimized -p mz-row-spine -- --ignored --nocapture bench_snapshot`;
618+ /// `BENCH_ROWS` and `BENCH_KEY_MOD` (0 for full-range keys) shape the input.
619+ #[ mz_ore:: test]
620+ #[ ignore]
621+ fn bench_snapshot_path ( ) {
622+ use differential_dataflow:: trace:: Builder ;
623+ use mz_repr:: Timestamp ;
624+ use std:: time:: Instant ;
625+
626+ let rows_n: usize = std:: env:: var ( "BENCH_ROWS" )
627+ . ok ( )
628+ . and_then ( |s| s. parse ( ) . ok ( ) )
629+ . unwrap_or ( 10_000_000 ) ;
630+ let key_mod: u64 = std:: env:: var ( "BENCH_KEY_MOD" )
631+ . ok ( )
632+ . and_then ( |s| s. parse ( ) . ok ( ) )
633+ . unwrap_or ( 0 ) ;
634+ let per_container = 200 ;
635+ let mut containers: Vec < Vec < ( ( Row , Row ) , Timestamp , i64 ) > > = Vec :: new ( ) ;
636+ let mut cur = Vec :: with_capacity ( per_container) ;
637+ for i in 0 ..rows_n {
638+ let mut k = u64:: cast_from ( i) . wrapping_mul ( 0x9E3779B97F4A7C15 ) >> 1 ;
639+ if key_mod > 0 {
640+ k %= key_mod;
641+ }
642+ let k = i64:: try_from ( k) . expect ( "fits after the shift" ) ;
643+ let key = Row :: pack_slice ( & [ Datum :: Int64 ( k) ] ) ;
644+ let val = Row :: pack_slice ( & [ Datum :: Int64 ( i64:: try_from ( i) . expect ( "row count fits" ) ) ] ) ;
645+ cur. push ( ( ( key, val) , Timestamp :: from ( 1u64 ) , 1i64 ) ) ;
646+ if cur. len ( ) == per_container {
647+ containers. push ( std:: mem:: take ( & mut cur) ) ;
648+ }
649+ }
650+ if !cur. is_empty ( ) {
651+ containers. push ( cur) ;
652+ }
653+ println ! (
654+ "{} rows in {} containers, key_mod {}" ,
655+ rows_n,
656+ containers. len( ) ,
657+ key_mod
658+ ) ;
659+ let upper = Antichain :: from_elem ( Timestamp :: from ( 2u64 ) ) ;
660+
661+ for round in 0 ..2 {
662+ let t = Instant :: now ( ) ;
663+ let mut ch: UnsortedChunker < ( Row , Row ) , Timestamp , i64 > = Default :: default ( ) ;
664+ let mut b: SnapshotBatcher < ( Row , Row ) , Timestamp , i64 > = Batcher :: new ( None , 0 ) ;
665+ for c in containers. iter ( ) {
666+ let mut c = c. clone ( ) ;
667+ ch. push_into ( & mut c) ;
668+ while let Some ( chunk) = ch. extract ( ) {
669+ b. push_into ( std:: mem:: take ( chunk) ) ;
670+ }
671+ }
672+ while let Some ( chunk) = ch. finish ( ) {
673+ b. push_into ( std:: mem:: take ( chunk) ) ;
674+ }
675+ let t_push = t. elapsed ( ) ;
676+ let ( mut chain, desc) = b. seal ( upper. clone ( ) ) ;
677+ let t_seal = t. elapsed ( ) - t_push;
678+ let batch = crate :: RowRowBuilder :: < Timestamp , i64 > :: seal ( & mut chain, desc) ;
679+ let t_build = t. elapsed ( ) - t_push - t_seal;
680+ println ! (
681+ "round {round}: chunk {:?} seal {:?} build {:?} total {:?} ({} updates)" ,
682+ t_push,
683+ t_seal,
684+ t_build,
685+ t. elapsed( ) ,
686+ differential_dataflow:: trace:: BatchReader :: len( & batch)
687+ ) ;
688+ }
689+ }
690+
691+ #[ mz_ore:: test]
692+ fn radix_sort_matches_comparison_sort ( ) {
693+ // Above the radix threshold, with two constant byte positions to skip and ties.
694+ let mut index: Vec < ( u64 , u32 , u32 ) > = ( 0 ..100_000u32 )
695+ . map ( |i| {
696+ let scrambled = u64:: from ( i) . wrapping_mul ( 0x9E3779B97F4A7C15 ) ;
697+ let prefix =
698+ ( 0x0009 << 48 ) | ( scrambled & 0x0000_00FF_FFFF_0000 ) | ( u64:: from ( i % 7 ) << 8 ) ;
699+ ( prefix, i / 1000 , i % 1000 )
700+ } )
701+ . collect ( ) ;
702+ let mut expected = index. clone ( ) ;
703+ expected. sort_unstable ( ) ;
704+ radix_sort_by_prefix ( & mut index) ;
705+ assert ! (
706+ index. windows( 2 ) . all( |w| w[ 0 ] . 0 <= w[ 1 ] . 0 ) ,
707+ "sorted by prefix"
708+ ) ;
709+ index. sort_unstable ( ) ;
710+ assert_eq ! ( index, expected, "a permutation of the input" ) ;
711+ }
712+
713+ #[ mz_ore:: test]
714+ fn equal_prefixes_are_ordered_by_full_comparison ( ) {
715+ // Two-column rows sharing the first column share the six-byte prefix; the second column
716+ // must still order them, and equal rows must still consolidate.
717+ let mut updates: Vec < ( ( Row , ( ) ) , u64 , i64 ) > = Vec :: new ( ) ;
718+ for y in [ 5i64 , -3 , 9 , 0 , 5 , 9 ] {
719+ let row = Row :: pack_slice ( & [ Datum :: Int64 ( 1 << 40 ) , Datum :: Int64 ( y) ] ) ;
720+ updates. push ( ( ( row, ( ) ) , 1 , 1 ) ) ;
721+ }
722+ let mut b = B :: new ( None , 0 ) ;
723+ let mut chunk = ColumnationStack :: with_capacity ( updates. len ( ) ) ;
724+ for u in & updates {
725+ chunk. copy ( u) ;
726+ }
727+ b. push_into ( chunk) ;
728+ let ( chain, _) = b. seal ( upper ( 2 ) ) ;
729+ let sealed: Vec < ( Row , i64 ) > = chain
730+ . iter ( )
731+ . flat_map ( |c| c. iter ( ) )
732+ . map ( |( ( k, ( ) ) , _, r) | ( k. clone ( ) , * r) )
733+ . collect ( ) ;
734+ let mut expected: Vec < ( Row , i64 ) > = Vec :: new ( ) ;
735+ let mut rows: Vec < Row > = updates. iter ( ) . map ( |( ( k, ( ) ) , _, _) | k. clone ( ) ) . collect ( ) ;
736+ rows. sort ( ) ;
737+ for row in rows {
738+ match expected. last_mut ( ) {
739+ Some ( ( prev, r) ) if * prev == row => * r += 1 ,
740+ _ => expected. push ( ( row, 1 ) ) ,
741+ }
742+ }
743+ assert_eq ! ( sealed, expected) ;
744+ }
745+
545746 #[ mz_ore:: test]
546747 fn sort_prefix_agrees_with_row_order ( ) {
547748 let rows: Vec < Row > = [
0 commit comments