@@ -744,8 +744,9 @@ func (b *BSI) MarshalBinary() ([][]byte, error) {
744744}
745745
746746// BatchEqual returns a bitmap containing the column IDs where the values are contained
747- // within the list of values provided. The parallelism parameter is accepted for API
748- // compatibility; work is shared across values, so the query runs on the calling goroutine.
747+ // within the list of values provided. The trie path shares work across values and runs
748+ // on the calling goroutine; on scattered queries that would fan the trie out it falls
749+ // back to a linear existence-bitmap scan, which parallelism splits across goroutines.
749750func (b * BSI ) BatchEqual (parallelism int , values []int64 ) * roaring.Bitmap {
750751 if b .eBM .IsEmpty () || len (values ) == 0 {
751752 return roaring .NewBitmap ()
@@ -773,13 +774,121 @@ func (b *BSI) BatchEqual(parallelism int, values []int64) *roaring.Bitmap {
773774 }
774775 sort .Slice (vals , func (i , j int ) bool { return vals [i ] < vals [j ] })
775776
777+ if len (vals ) >= 128 && b .shouldUseParallelScan (vals , bitCount ) {
778+ result := b .parallelBatchEqualScan (parallelism , vals )
779+ if b .runOptimized {
780+ result .RunOptimize ()
781+ }
782+ return result
783+ }
784+
776785 result := b .matchTrie (vals , bitCount - 1 , b .eBM , false )
777786 if b .runOptimized {
778787 result .RunOptimize ()
779788 }
780789 return result
781790}
782791
792+ // shouldUseParallelScan reports whether BatchEqual should skip the match trie in
793+ // favor of a linear existence-bitmap scan. estimateBranchCount caps the trie's
794+ // branch fan-out; past the crossover the trie degenerates into an intermediate-
795+ // bitmap blowup, and only then is the scan's goroutine cost worth paying. The
796+ // scan also needs a large existence bitmap for the parallelism to earn its keep.
797+ func (b * BSI ) shouldUseParallelScan (vals []uint64 , bitCount int ) bool {
798+ return estimateBranchCount (vals , bitCount - 1 , 64 ) >= 64 && b .eBM .GetCardinality () >= 100000
799+ }
800+
801+ // estimateBranchCount bounds the branches the match trie would take on vals over
802+ // planes [0, p], stopping once the count reaches limit. Perfectly contiguous
803+ // ranges collapse to zero. It reads sorted query values only, so the estimate
804+ // costs nothing against the data.
805+ func estimateBranchCount (vals []uint64 , p int , limit int ) int {
806+ if len (vals ) <= 1 || limit <= 0 {
807+ return 0
808+ }
809+ if vals [len (vals )- 1 ]- vals [0 ] == uint64 (len (vals )- 1 ) {
810+ return 0
811+ }
812+ if p >= 64 {
813+ p = 63
814+ }
815+ if p < 0 || (p < 63 && uint64 (len (vals )) == uint64 (1 )<< uint (p + 1 )) {
816+ return 0
817+ }
818+ mask := uint64 (1 ) << uint (p )
819+ cut := sort .Search (len (vals ), func (i int ) bool {
820+ return vals [i ]& mask != 0
821+ })
822+ if cut == 0 || cut == len (vals ) {
823+ return estimateBranchCount (vals , p - 1 , limit )
824+ }
825+ leftLimit := limit - 1
826+ leftBranch := estimateBranchCount (vals [:cut ], p - 1 , leftLimit )
827+ rightLimit := leftLimit - leftBranch
828+ rightBranch := estimateBranchCount (vals [cut :], p - 1 , rightLimit )
829+ return 1 + leftBranch + rightBranch
830+ }
831+
832+ // parallelBatchEqualScan computes BatchEqual by a flat membership scan: it reads
833+ // each existing column's value with GetValue and keeps the ones present in vals.
834+ // The existence bitmap is authoritative, so the result is always a subset of it.
835+ // This trades the trie's intermediate-bitmap allocations for one pass over the
836+ // data, which wins when the query fans the trie out (see shouldUseParallelScan).
837+ func (b * BSI ) parallelBatchEqualScan (parallelism int , vals []uint64 ) * roaring.Bitmap {
838+ want := make (map [uint64 ]struct {}, len (vals ))
839+ for _ , v := range vals {
840+ want [v ] = struct {}{}
841+ }
842+
843+ n := parallelism
844+ if n <= 0 {
845+ n = runtime .NumCPU ()
846+ }
847+ card := b .eBM .GetCardinality ()
848+ if card == 0 {
849+ return roaring .NewBitmap ()
850+ }
851+ if uint64 (n ) > card {
852+ n = int (card )
853+ }
854+
855+ x := card / uint64 (n )
856+ remainder := card - (x * uint64 (n ))
857+ iter := b .eBM .ManyIterator ()
858+ resultsChan := make (chan * roaring.Bitmap , n )
859+
860+ var wg sync.WaitGroup
861+ for i := 0 ; i < n ; i ++ {
862+ size := x
863+ if i == n - 1 {
864+ size += remainder
865+ }
866+ batch := make ([]uint32 , size )
867+ iter .NextMany (batch )
868+ wg .Add (1 )
869+ go func (cols []uint32 ) {
870+ defer wg .Done ()
871+ out := roaring .NewBitmap ()
872+ for _ , col := range cols {
873+ if v , ok := b .GetValue (uint64 (col )); ok {
874+ if _ , hit := want [uint64 (v )]; hit {
875+ out .Add (col )
876+ }
877+ }
878+ }
879+ resultsChan <- out
880+ }(batch )
881+ }
882+ wg .Wait ()
883+ close (resultsChan )
884+
885+ ba := make ([]* roaring.Bitmap , 0 , n )
886+ for bm := range resultsChan {
887+ ba = append (ba , bm )
888+ }
889+ return roaring .ParOr (0 , ba ... )
890+ }
891+
783892// matchTrie returns the columns in prefix whose bits on planes [0, p] equal the low
784893// p+1 bits of any value in vals. vals must be unique, sorted, and share identical bits
785894// above plane p; prefix holds the columns matching those shared upper bits, so results
0 commit comments