Skip to content

Commit f60d87c

Browse files
Add AVX2 bitmap intersection stores
1 parent 4de5359 commit f60d87c

7 files changed

Lines changed: 227 additions & 9 deletions

and_slices.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
package roaring
2+
3+
func andStoreSliceGo(dst, s, m []uint64) {
4+
for i := range dst {
5+
dst[i] = s[i] & m[i]
6+
}
7+
}

and_slices_amd64.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
//go:build amd64 && !appengine
2+
// +build amd64,!appengine
3+
4+
package roaring
5+
6+
//go:noescape
7+
func _andStoreSliceAVX2(dst, s, m []uint64)
8+
9+
func andStoreSlice(dst, s, m []uint64) {
10+
if useAVX2 {
11+
_andStoreSliceAVX2(dst, s, m)
12+
return
13+
}
14+
andStoreSliceGo(dst, s, m)
15+
}

and_slices_generic.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
//go:build !amd64 || appengine
2+
// +build !amd64 appengine
3+
4+
package roaring
5+
6+
func andStoreSlice(dst, s, m []uint64) {
7+
andStoreSliceGo(dst, s, m)
8+
}

and_slices_test.go

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
package roaring
2+
3+
import "testing"
4+
5+
func TestAndStoreSlice(t *testing.T) {
6+
for _, n := range []int{0, 1, 3, 4, 5, bitmapContainerSize} {
7+
left := make([]uint64, n)
8+
right := make([]uint64, n)
9+
for i := range left {
10+
left[i] = uint64(i+1) * 0x5555555555555555
11+
right[i] = ^uint64(i * 3)
12+
}
13+
14+
got := make([]uint64, n)
15+
andStoreSlice(got, left, right)
16+
for i := range got {
17+
want := left[i] & right[i]
18+
if got[i] != want {
19+
t.Fatalf("separate destination len=%d index=%d: got %#x, want %#x", n, i, got[i], want)
20+
}
21+
}
22+
23+
inPlace := append([]uint64(nil), left...)
24+
andStoreSlice(inPlace, inPlace, right)
25+
for i := range inPlace {
26+
want := left[i] & right[i]
27+
if inPlace[i] != want {
28+
t.Fatalf("in-place destination len=%d index=%d: got %#x, want %#x", n, i, inPlace[i], want)
29+
}
30+
}
31+
}
32+
}

bitmapcontainer.go

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -867,19 +867,21 @@ func (bc *bitmapContainer) getCardinalityInRange(start, end uint) int {
867867
func (bc *bitmapContainer) andBitmap(value2 *bitmapContainer) container {
868868
newcardinality := int(popcntAndSlice(bc.bitmap, value2.bitmap))
869869
if newcardinality > arrayDefaultMaxSize {
870-
answer := newBitmapContainer()
871-
for k := 0; k < len(answer.bitmap); k++ {
872-
answer.bitmap[k] = bc.bitmap[k] & value2.bitmap[k]
873-
}
874-
answer.cardinality = newcardinality
875-
return answer
870+
return andBitmapStore(bc, value2, newcardinality)
876871
}
877872
ac := newArrayContainerSize(newcardinality)
878873
fillArrayAND(ac.content, bc.bitmap, value2.bitmap)
879874
ac.content = ac.content[:newcardinality]
880875
return ac
881876
}
882877

878+
func andBitmapStore(left, right *bitmapContainer, cardinality int) *bitmapContainer {
879+
answer := newBitmapContainer()
880+
andStoreSlice(answer.bitmap, left.bitmap, right.bitmap)
881+
answer.cardinality = cardinality
882+
return answer
883+
}
884+
883885
func (bc *bitmapContainer) intersectsArray(value2 *arrayContainer) bool {
884886
c := value2.getCardinality()
885887
for k := 0; k < c; k++ {
@@ -902,9 +904,7 @@ func (bc *bitmapContainer) intersectsBitmap(value2 *bitmapContainer) bool {
902904

903905
func (bc *bitmapContainer) iandBitmap(value2 *bitmapContainer) container {
904906
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-
}
907+
andStoreSlice(bc.bitmap, bc.bitmap, value2.bitmap)
908908
bc.cardinality = newcardinality
909909

910910
if newcardinality <= arrayDefaultMaxSize {

popcnt_avx2_amd64.s

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -358,3 +358,42 @@ TEXT ·_hasAVX2(SB), NOSPLIT, $0-1
358358
noavx2:
359359
SETEQ ret+0(FP) // ZF is still set by whichever TESTL ran last
360360
RET
361+
362+
// func _andStoreSliceAVX2(dst, s, m []uint64)
363+
//
364+
// Writes s[i] & m[i] to dst[i]. The three slices must have equal lengths.
365+
// Four uint64 words are processed per AVX2 iteration; a scalar tail handles
366+
// any remaining words.
367+
TEXT ·_andStoreSliceAVX2(SB), NOSPLIT, $0-72
368+
MOVQ dst_base+0(FP), DI
369+
MOVQ s_base+24(FP), SI
370+
MOVQ m_base+48(FP), DX
371+
MOVQ dst_len+8(FP), CX
372+
373+
MOVQ CX, R8
374+
SHRQ $2, R8
375+
JZ andstoretail
376+
andstoreloop:
377+
VMOVDQU (SI), Y0
378+
VPAND (DX), Y0, Y0
379+
VMOVDQU Y0, (DI)
380+
ADDQ $32, DI
381+
ADDQ $32, SI
382+
ADDQ $32, DX
383+
DECQ R8
384+
JNZ andstoreloop
385+
andstoretail:
386+
ANDL $3, CX
387+
JZ andstoredone
388+
andstoretailloop:
389+
MOVQ (SI), AX
390+
ANDQ (DX), AX
391+
MOVQ AX, (DI)
392+
ADDQ $8, DI
393+
ADDQ $8, SI
394+
ADDQ $8, DX
395+
DECL CX
396+
JNZ andstoretailloop
397+
andstoredone:
398+
VZEROUPPER
399+
RET

popcnt_avx2_amd64_test.go

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,3 +127,120 @@ func TestAVX2PopcntDifferential(t *testing.T) {
127127
}
128128
}
129129
}
130+
131+
func TestAVX2AndStoreSliceDispatch(t *testing.T) {
132+
saved := useAVX2
133+
defer func() { useAVX2 = saved }()
134+
135+
r := rand.New(rand.NewSource(11))
136+
for _, on := range []bool{false, true} {
137+
if on && !saved {
138+
continue // CPU has no AVX2; only the fallback exists
139+
}
140+
useAVX2 = on
141+
for _, n := range avx2TestLengths {
142+
s := randomUint64Slice(r, n)
143+
m := randomUint64Slice(r, n)
144+
expected := append([]uint64(nil), s...)
145+
andStoreSliceGo(expected, s, m)
146+
147+
dst := append([]uint64(nil), s...)
148+
andStoreSlice(dst, s, m)
149+
assert.Equalf(t, expected, dst, "separate destination avx2=%v len=%d", on, n)
150+
151+
inPlaceLeft := append([]uint64(nil), s...)
152+
andStoreSlice(inPlaceLeft, inPlaceLeft, m)
153+
assert.Equalf(t, expected, inPlaceLeft, "left alias avx2=%v len=%d", on, n)
154+
155+
inPlaceRight := append([]uint64(nil), m...)
156+
andStoreSlice(inPlaceRight, s, inPlaceRight)
157+
assert.Equalf(t, expected, inPlaceRight, "right alias avx2=%v len=%d", on, n)
158+
}
159+
}
160+
}
161+
162+
func TestAVX2AndStoreSliceDifferential(t *testing.T) {
163+
if !useAVX2 {
164+
t.Skip("AVX2 not available on this CPU")
165+
}
166+
r := rand.New(rand.NewSource(13))
167+
for _, n := range avx2TestLengths {
168+
for iter := 0; iter < 64; iter++ {
169+
s := randomUint64Slice(r, n)
170+
m := randomUint64Slice(r, n)
171+
expected := append([]uint64(nil), s...)
172+
andStoreSliceGo(expected, s, m)
173+
174+
dst := append([]uint64(nil), s...)
175+
_andStoreSliceAVX2(dst, s, m)
176+
assert.Equalf(t, expected, dst, "separate destination len=%d", n)
177+
178+
inPlaceLeft := append([]uint64(nil), s...)
179+
_andStoreSliceAVX2(inPlaceLeft, inPlaceLeft, m)
180+
assert.Equalf(t, expected, inPlaceLeft, "left alias len=%d", n)
181+
182+
inPlaceRight := append([]uint64(nil), m...)
183+
_andStoreSliceAVX2(inPlaceRight, s, inPlaceRight)
184+
assert.Equalf(t, expected, inPlaceRight, "right alias len=%d", n)
185+
}
186+
}
187+
}
188+
189+
func TestAVX2BitmapAndStoreDispatch(t *testing.T) {
190+
saved := useAVX2
191+
defer func() { useAVX2 = saved }()
192+
193+
newFull := func() *bitmapContainer {
194+
bc := newBitmapContainer()
195+
for i := range bc.bitmap {
196+
bc.bitmap[i] = ^uint64(0)
197+
}
198+
bc.cardinality = maxCapacity
199+
return bc
200+
}
201+
202+
right := newBitmapContainer()
203+
for i := range right.bitmap {
204+
right.bitmap[i] = 0xaaaaaaaaaaaaaaaa
205+
}
206+
right.cardinality = maxCapacity / 2
207+
208+
small := newBitmapContainer()
209+
small.bitmap[0] = 1
210+
small.cardinality = 1
211+
212+
for _, on := range []bool{false, true} {
213+
if on && !saved {
214+
continue // CPU has no AVX2; only the fallback exists
215+
}
216+
useAVX2 = on
217+
218+
answer, ok := newFull().andBitmap(right).(*bitmapContainer)
219+
if !ok {
220+
t.Fatalf("expected bitmap result with avx2=%v", on)
221+
}
222+
assert.Equalf(t, right.bitmap, answer.bitmap, "bitmap result avx2=%v", on)
223+
assert.Equalf(t, right.cardinality, answer.cardinality, "bitmap cardinality avx2=%v", on)
224+
225+
inPlace := newFull()
226+
answer, ok = inPlace.iandBitmap(right).(*bitmapContainer)
227+
if !ok {
228+
t.Fatalf("expected in-place bitmap result with avx2=%v", on)
229+
}
230+
assert.Samef(t, inPlace, answer, "in-place bitmap result avx2=%v", on)
231+
assert.Equalf(t, right.bitmap, inPlace.bitmap, "in-place bitmap contents avx2=%v", on)
232+
233+
arrayResult, ok := newFull().andBitmap(small).(*arrayContainer)
234+
if !ok {
235+
t.Fatalf("expected array result with avx2=%v", on)
236+
}
237+
assert.Equalf(t, []uint16{0}, arrayResult.content, "array result avx2=%v", on)
238+
239+
inPlace = newFull()
240+
arrayResult, ok = inPlace.iandBitmap(small).(*arrayContainer)
241+
if !ok {
242+
t.Fatalf("expected in-place array result with avx2=%v", on)
243+
}
244+
assert.Equalf(t, []uint16{0}, arrayResult.content, "in-place array result avx2=%v", on)
245+
}
246+
}

0 commit comments

Comments
 (0)