@@ -521,9 +521,8 @@ fn hash_generic_byte_view_array<T: ByteViewType>(
521521#[ cfg( not( feature = "force_hash_collisions" ) ) ]
522522const DICT_SCATTER_LANES : usize = 8 ;
523523
524- /// Fold `dict_hash` into `prev` when `MULTI_COL` (secondary column), else
525- /// overwrite with `dict_hash` (first column). Const generic +
526- /// `#[inline(always)]` makes it branch-free per instantiation.
524+ /// Fold `dict_hash` into `prev` when `MULTI_COL`, else
525+ /// overwrite with `dict_hash`.
527526#[ cfg( not( feature = "force_hash_collisions" ) ) ]
528527#[ inline( always) ]
529528fn maybe_combine_dict_hash < const MULTI_COL : bool > ( prev : u64 , dict_hash : u64 ) -> u64 {
@@ -540,9 +539,8 @@ fn maybe_combine_dict_hash<const MULTI_COL: bool>(prev: u64, dict_hash: u64) ->
540539/// The 8-wide unroll is for performance, splitting the loop into
541540/// `std::array::from_fn` (gather 8 dict hashes by key index) followed by a
542541/// fixed-length inner loop encourages the compiler to overlap 8
543- /// independent load latencies, saturating the gather pipeline on
544- /// ARM64/x86 instead of serializing one lookup at a time. The scalar tail
545- /// handles the non-multiple-of-8 remainder.
542+ /// independent load latencies, saturating the gather pipeline instead of
543+ /// serializing one lookup at a time.
546544///
547545/// Invariants: `keys.len() == hashes.len()`; no key or dict value referenced
548546/// here is null.
@@ -571,6 +569,7 @@ fn scatter_dict_hashes_no_null_checks<
571569 maybe_combine_dict_hash :: < MULTI_COL > ( hash_group[ lane] , lane_hashes[ lane] ) ;
572570 }
573571 }
572+ // need to handle the remainder for non 64 byte multiples
574573 for ( key, hash) in keys_tail. iter ( ) . zip ( hashes_tail. iter_mut ( ) ) {
575574 let dict_hash = unsafe { * dict_hashes. get_unchecked ( key. as_usize ( ) ) } ;
576575 * hash = maybe_combine_dict_hash :: < MULTI_COL > ( * hash, dict_hash) ;
@@ -582,7 +581,7 @@ fn scatter_dict_hashes_no_null_checks<
582581/// nullable-values case; the per-lane `is_valid` check is unavoidable.
583582#[ cfg( not( feature = "force_hash_collisions" ) ) ]
584583#[ inline( always) ]
585- fn scatter_dict_hashes_skip_null_values <
584+ fn scatter_dict_hashes_skip_value_nulls <
586585 K : ArrowDictionaryKeyType ,
587586 const MULTI_COL : bool ,
588587> (
@@ -600,10 +599,7 @@ fn scatter_dict_hashes_skip_null_values<
600599 }
601600}
602601
603- /// Scatter dict-value hashes inside a mixed-validity 64-key chunk, with
604- /// per-lane key-validity given by `mask`. All-valid-values path: branchless
605- /// 64-lane gather + csel-blend by `mask`. Possibly-null-values path:
606- /// bit-walk set lanes via `trailing_zeros`.
602+ /// Scatter inside a mixed-validity 64-key chunk governed by `mask`.
607603#[ cfg( not( feature = "force_hash_collisions" ) ) ]
608604#[ inline( always) ]
609605fn scatter_dict_hashes_in_masked_chunk <
@@ -633,8 +629,10 @@ fn scatter_dict_hashes_in_masked_chunk<
633629 }
634630 return ;
635631 }
632+ // null-key lanes carry garbage indices; we gather them anyway and mask the result out below
636633 let max_valid_idx = dict_hashes. len ( ) . saturating_sub ( 1 ) ;
637634 for lane_base in ( 0 ..64 ) . step_by ( DICT_SCATTER_LANES ) {
635+ // gather 8 dict hashes in parallel; clamp keeps garbage indices in-bounds
638636 let lane_hashes: [ u64 ; DICT_SCATTER_LANES ] = std:: array:: from_fn ( |lane_offset| {
639637 let raw_idx =
640638 unsafe { keys. get_unchecked ( lane_base + lane_offset) . as_usize ( ) } ;
@@ -653,10 +651,10 @@ fn scatter_dict_hashes_in_masked_chunk<
653651
654652/// Scatter precomputed dictionary value hashes to key positions.
655653///
656- /// Const generics eliminate runtime branching:
657- /// - `HAS_NULL_KEYS`: dictionary has null keys to skip
658- /// - `HAS_NULL_VALUES`: dictionary values may be null (per-index check)
659- /// - `MULTI_COL`: combine with existing hash vs. overwrite
654+ /// Uses const generics to eliminate runtime branching in the hot loop :
655+ /// - `HAS_NULL_KEYS`: Whether to check for null dictionary keys
656+ /// - `HAS_NULL_VALUES`: Whether to check for null dictionary values
657+ /// - `MULTI_COL`: Whether to combine with existing hash (true) or initialize (false)
660658#[ cfg( not( feature = "force_hash_collisions" ) ) ]
661659#[ inline( never) ]
662660fn hash_dictionary_scatter <
@@ -674,7 +672,7 @@ fn hash_dictionary_scatter<
674672
675673 if !HAS_NULL_KEYS {
676674 if HAS_NULL_VALUES {
677- scatter_dict_hashes_skip_null_values :: < K , MULTI_COL > (
675+ scatter_dict_hashes_skip_value_nulls :: < K , MULTI_COL > (
678676 keys,
679677 dict_values. as_ref ( ) ,
680678 dict_hashes,
@@ -694,13 +692,15 @@ fn hash_dictionary_scatter<
694692 let bit_chunks = null_buffer. inner ( ) . bit_chunks ( ) ;
695693 let chunk_len = bit_chunks. chunk_len ( ) ;
696694
695+ // Walk the key null buffer 64 bits at a time; bit i set = lane i's key is non-null.
697696 for ( chunk_idx, mask) in bit_chunks. iter ( ) . enumerate ( ) {
698697 let base = chunk_idx * 64 ;
699698 let keys_chunk = & keys[ base..base + 64 ] ;
700699 let hashes_chunk = & mut hashes_buffer[ base..base + 64 ] ;
701700 if mask == u64:: MAX {
701+ // all 64 keys valid: dense helper
702702 if HAS_NULL_VALUES {
703- scatter_dict_hashes_skip_null_values :: < K , MULTI_COL > (
703+ scatter_dict_hashes_skip_value_nulls :: < K , MULTI_COL > (
704704 keys_chunk,
705705 dict_values. as_ref ( ) ,
706706 dict_hashes,
@@ -714,6 +714,7 @@ fn hash_dictionary_scatter<
714714 ) ;
715715 }
716716 } else if mask != 0 {
717+ // mixed: masked-chunk helper (mask == 0 falls through, nothing to write)
717718 scatter_dict_hashes_in_masked_chunk :: < K , HAS_NULL_VALUES , MULTI_COL > (
718719 keys_chunk,
719720 dict_values. as_ref ( ) ,
@@ -724,6 +725,7 @@ fn hash_dictionary_scatter<
724725 }
725726 }
726727
728+ // non-64 multiple: scalar path for the tail
727729 let remainder_len = bit_chunks. remainder_len ( ) ;
728730 if remainder_len > 0 {
729731 let remainder = bit_chunks. remainder_bits ( ) ;
0 commit comments