Skip to content

Commit ff94de3

Browse files
committed
Vectorize the bitmap-container word operations, fusing the cardinality pass
The bitmap-container boolean operations wrote their result with a plain Go loop over 1024 words and then made a second pass to count it. Add six word helpers -- orSlice, andSlice, xorSlice, andNotSlice, and the orCardSlice and andCardSlice variants that also return the population count via VPOPCNTQ -- and use them to replace eight hand-written loops in bitmapcontainer.go. orBitmap, iorBitmap and iandBitmap now make a single fused pass. andBitmap, xorBitmap, ixorBitmap, andNotBitmap and iandNotBitmapSurely need the cardinality before choosing a bitmap or array result, so they keep the count-first shape and only their write loop is vectorized. lazyIORBitmap and lazyORBitmap lose their manual four-way unroll. dst may alias either source: sources are loaded before the destination is stored within an iteration, which is what the in-place operations rely on. One 8 KB container on a Xeon Gold 6548N: write only 599 ns -> 52 ns (11.5x) write + cardinality 631 ns -> 115 ns (5.5x) Two bitmaps of 64 dense bitmap containers: Or 152985 ns -> 90806 ns (1.68x) AndNot 153366 ns -> 105304 ns (1.46x) And 152147 ns -> 105404 ns (1.44x) Xor 151482 ns -> 105158 ns (1.44x) The end-to-end gap is allocation: each operation allocates 64 fresh 8 KB containers. Claude-Session: https://claude.ai/code/session_0123ePBefqPjrCvhxdwWFakj
1 parent 92bd7e5 commit ff94de3

8 files changed

Lines changed: 721 additions & 49 deletions

bitmapcontainer.go

Lines changed: 10 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -562,10 +562,7 @@ func (bc *bitmapContainer) orArrayCardinality(value2 *arrayContainer) int {
562562

563563
func (bc *bitmapContainer) orBitmap(value2 *bitmapContainer) container {
564564
answer := newBitmapContainer()
565-
for k := 0; k < len(answer.bitmap); k++ {
566-
answer.bitmap[k] = bc.bitmap[k] | value2.bitmap[k]
567-
}
568-
answer.computeCardinality()
565+
answer.cardinality = int(orCardSlice(answer.bitmap, bc.bitmap, value2.bitmap))
569566
if answer.isFull() {
570567
return newRunContainer16Range(0, MaxUint16)
571568
}
@@ -601,11 +598,7 @@ func (bc *bitmapContainer) iorArray(ac *arrayContainer) container {
601598

602599
func (bc *bitmapContainer) iorBitmap(value2 *bitmapContainer) container {
603600
answer := bc
604-
answer.cardinality = 0
605-
for k := 0; k < len(answer.bitmap); k++ {
606-
answer.bitmap[k] = bc.bitmap[k] | value2.bitmap[k]
607-
}
608-
answer.computeCardinality()
601+
answer.cardinality = int(orCardSlice(answer.bitmap, bc.bitmap, value2.bitmap))
609602
if bc.isFull() {
610603
return newRunContainer16Range(0, MaxUint16)
611604
}
@@ -654,16 +647,7 @@ func (bc *bitmapContainer) lazyIORBitmap(value2 *bitmapContainer) container {
654647
bitmap := answer.bitmap
655648
other := value2.bitmap
656649

657-
// Bitmap containers always span bitmapContainerSize words. Prove the
658-
// bounds once so the compiler can eliminate the checks in the unrolled loop.
659-
_ = bitmap[bitmapContainerSize-1]
660-
_ = other[bitmapContainerSize-1]
661-
for k := 0; k < bitmapContainerSize; k += 4 {
662-
bitmap[k] |= other[k]
663-
bitmap[k+1] |= other[k+1]
664-
bitmap[k+2] |= other[k+2]
665-
bitmap[k+3] |= other[k+3]
666-
}
650+
orSlice(bitmap, bitmap, other)
667651
answer.cardinality = invalidCardinality
668652
return answer
669653
}
@@ -674,17 +658,7 @@ func (bc *bitmapContainer) lazyORBitmap(value2 *bitmapContainer) container {
674658
left := bc.bitmap
675659
right := value2.bitmap
676660

677-
// Bitmap containers always span bitmapContainerSize words. Prove the
678-
// bounds once so the compiler can eliminate the checks in the unrolled loop.
679-
_ = bitmap[bitmapContainerSize-1]
680-
_ = left[bitmapContainerSize-1]
681-
_ = right[bitmapContainerSize-1]
682-
for k := 0; k < bitmapContainerSize; k += 4 {
683-
bitmap[k] = left[k] | right[k]
684-
bitmap[k+1] = left[k+1] | right[k+1]
685-
bitmap[k+2] = left[k+2] | right[k+2]
686-
bitmap[k+3] = left[k+3] | right[k+3]
687-
}
661+
orSlice(bitmap, left, right)
688662
answer.cardinality = invalidCardinality
689663
return answer
690664
}
@@ -744,9 +718,7 @@ func (bc *bitmapContainer) xorBitmap(value2 *bitmapContainer) container {
744718

745719
if newCardinality > arrayDefaultMaxSize {
746720
answer := newBitmapContainer()
747-
for k := 0; k < len(answer.bitmap); k++ {
748-
answer.bitmap[k] = bc.bitmap[k] ^ value2.bitmap[k]
749-
}
721+
xorSlice(answer.bitmap, bc.bitmap, value2.bitmap)
750722
answer.cardinality = newCardinality
751723
if answer.isFull() {
752724
return newRunContainer16Range(0, MaxUint16)
@@ -868,9 +840,7 @@ func (bc *bitmapContainer) andBitmap(value2 *bitmapContainer) container {
868840
newcardinality := int(popcntAndSlice(bc.bitmap, value2.bitmap))
869841
if newcardinality > arrayDefaultMaxSize {
870842
answer := newBitmapContainer()
871-
for k := 0; k < len(answer.bitmap); k++ {
872-
answer.bitmap[k] = bc.bitmap[k] & value2.bitmap[k]
873-
}
843+
andSlice(answer.bitmap, bc.bitmap, value2.bitmap)
874844
answer.cardinality = newcardinality
875845
return answer
876846
}
@@ -901,10 +871,7 @@ func (bc *bitmapContainer) intersectsBitmap(value2 *bitmapContainer) bool {
901871
}
902872

903873
func (bc *bitmapContainer) iandBitmap(value2 *bitmapContainer) container {
904-
newcardinality := int(popcntAndSlice(bc.bitmap, value2.bitmap))
905-
for k := 0; k < len(bc.bitmap); k++ {
906-
bc.bitmap[k] = bc.bitmap[k] & value2.bitmap[k]
907-
}
874+
newcardinality := int(andCardSlice(bc.bitmap, bc.bitmap, value2.bitmap))
908875
bc.cardinality = newcardinality
909876

910877
if newcardinality <= arrayDefaultMaxSize {
@@ -938,9 +905,7 @@ func (bc *bitmapContainer) ixorRun16(value2 *runContainer16) container {
938905
func (bc *bitmapContainer) ixorBitmap(value2 *bitmapContainer) container {
939906
newCardinality := int(popcntXorSlice(bc.bitmap, value2.bitmap))
940907
if newCardinality > arrayDefaultMaxSize {
941-
for k := 0; k < len(bc.bitmap); k++ {
942-
bc.bitmap[k] = bc.bitmap[k] ^ value2.bitmap[k]
943-
}
908+
xorSlice(bc.bitmap, bc.bitmap, value2.bitmap)
944909
bc.cardinality = newCardinality
945910
return bc
946911
}
@@ -1064,9 +1029,7 @@ func (bc *bitmapContainer) andNotBitmap(value2 *bitmapContainer) container {
10641029
newCardinality := int(popcntMaskSlice(bc.bitmap, value2.bitmap))
10651030
if newCardinality > arrayDefaultMaxSize {
10661031
answer := newBitmapContainer()
1067-
for k := 0; k < len(answer.bitmap); k++ {
1068-
answer.bitmap[k] = bc.bitmap[k] &^ value2.bitmap[k]
1069-
}
1032+
andNotSlice(answer.bitmap, bc.bitmap, value2.bitmap)
10701033
answer.cardinality = newCardinality
10711034
return answer
10721035
}
@@ -1077,9 +1040,7 @@ func (bc *bitmapContainer) andNotBitmap(value2 *bitmapContainer) container {
10771040

10781041
func (bc *bitmapContainer) iandNotBitmapSurely(value2 *bitmapContainer) container {
10791042
newCardinality := int(popcntMaskSlice(bc.bitmap, value2.bitmap))
1080-
for k := 0; k < len(bc.bitmap); k++ {
1081-
bc.bitmap[k] = bc.bitmap[k] &^ value2.bitmap[k]
1082-
}
1043+
andNotSlice(bc.bitmap, bc.bitmap, value2.bitmap)
10831044
bc.cardinality = newCardinality
10841045
if bc.getCardinality() <= arrayDefaultMaxSize {
10851046
return bc.toArrayContainer()

bitsetops.go

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
package roaring
2+
3+
import "math/bits"
4+
5+
// Portable implementations of the bitmap-container word operations. Each writes
6+
// dst[i] = a[i] op b[i]; the Card variants also return the population count of
7+
// the result, which callers would otherwise obtain with a second pass.
8+
//
9+
// dst may alias a or b: every element is read before it is written.
10+
11+
func orSliceGo(dst, a, b []uint64) {
12+
for i := range dst {
13+
dst[i] = a[i] | b[i]
14+
}
15+
}
16+
17+
func andSliceGo(dst, a, b []uint64) {
18+
for i := range dst {
19+
dst[i] = a[i] & b[i]
20+
}
21+
}
22+
23+
func xorSliceGo(dst, a, b []uint64) {
24+
for i := range dst {
25+
dst[i] = a[i] ^ b[i]
26+
}
27+
}
28+
29+
func andNotSliceGo(dst, a, b []uint64) {
30+
for i := range dst {
31+
dst[i] = a[i] &^ b[i]
32+
}
33+
}
34+
35+
func orCardSliceGo(dst, a, b []uint64) uint64 {
36+
card := 0
37+
for i := range dst {
38+
v := a[i] | b[i]
39+
dst[i] = v
40+
card += bits.OnesCount64(v)
41+
}
42+
return uint64(card)
43+
}
44+
45+
func andCardSliceGo(dst, a, b []uint64) uint64 {
46+
card := 0
47+
for i := range dst {
48+
v := a[i] & b[i]
49+
dst[i] = v
50+
card += bits.OnesCount64(v)
51+
}
52+
return uint64(card)
53+
}

bitsetops_avx512_amd64.go

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
//go:build amd64 && !appengine
2+
// +build amd64,!appengine
3+
4+
package roaring
5+
6+
import "golang.org/x/sys/cpu"
7+
8+
// The functions below are implemented in bitsetops_avx512_amd64.s. The Card
9+
// variants fuse the write with the population count of the result, so a
10+
// bitmap-container operation that needs both makes one pass over the container
11+
// instead of two.
12+
13+
//go:noescape
14+
func orSliceAVX512(dst, a, b []uint64)
15+
16+
//go:noescape
17+
func andSliceAVX512(dst, a, b []uint64)
18+
19+
//go:noescape
20+
func xorSliceAVX512(dst, a, b []uint64)
21+
22+
//go:noescape
23+
func andNotSliceAVX512(dst, a, b []uint64)
24+
25+
//go:noescape
26+
func orCardSliceAVX512(dst, a, b []uint64) uint64
27+
28+
//go:noescape
29+
func andCardSliceAVX512(dst, a, b []uint64) uint64
30+
31+
// useAVX512BitsetOps requires AVX512_VPOPCNTDQ because the fused kernels use
32+
// VPOPCNTQ; the plain writes only need AVX512F, but they are gated together so
33+
// a single flag governs the whole file. x/sys/cpu verifies operating-system
34+
// support for the ZMM state and honors GODEBUG=cpu.avx512vpopcntdq=off.
35+
var useAVX512BitsetOps = cpu.X86.HasAVX512VPOPCNTDQ
36+
37+
func orSlice(dst, a, b []uint64) {
38+
if useAVX512BitsetOps {
39+
orSliceAVX512(dst, a, b)
40+
return
41+
}
42+
orSliceGo(dst, a, b)
43+
}
44+
45+
func andSlice(dst, a, b []uint64) {
46+
if useAVX512BitsetOps {
47+
andSliceAVX512(dst, a, b)
48+
return
49+
}
50+
andSliceGo(dst, a, b)
51+
}
52+
53+
func xorSlice(dst, a, b []uint64) {
54+
if useAVX512BitsetOps {
55+
xorSliceAVX512(dst, a, b)
56+
return
57+
}
58+
xorSliceGo(dst, a, b)
59+
}
60+
61+
func andNotSlice(dst, a, b []uint64) {
62+
if useAVX512BitsetOps {
63+
andNotSliceAVX512(dst, a, b)
64+
return
65+
}
66+
andNotSliceGo(dst, a, b)
67+
}
68+
69+
func orCardSlice(dst, a, b []uint64) uint64 {
70+
if useAVX512BitsetOps {
71+
return orCardSliceAVX512(dst, a, b)
72+
}
73+
// Without a vector population count the fused loop is no faster than the
74+
// two passes the callers used before, and popcntSlice may itself be AVX2.
75+
orSliceGo(dst, a, b)
76+
return popcntSlice(dst)
77+
}
78+
79+
func andCardSlice(dst, a, b []uint64) uint64 {
80+
if useAVX512BitsetOps {
81+
return andCardSliceAVX512(dst, a, b)
82+
}
83+
andSliceGo(dst, a, b)
84+
return popcntSlice(dst)
85+
}

0 commit comments

Comments
 (0)