Skip to content

Commit 44559cd

Browse files
authored
Merge pull request #534 from perfloop/perfloop-pr-open-gtf1q33kjy
perf: optimize bitmapContainer.fillLeastSignificant16bits using TrailingZeros64
2 parents c260295 + cfa8937 commit 44559cd

3 files changed

Lines changed: 137 additions & 4 deletions

File tree

bitmapcontainer.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -295,16 +295,16 @@ func bitmapEquals(a, b []uint64) bool {
295295
}
296296

297297
func (bc *bitmapContainer) fillLeastSignificant16bits(x []uint32, i int, mask uint32) int {
298-
// TODO: should be written as optimized assembly
298+
// On amd64 this loop compiles to TZCNT/BLSR; the remaining headroom is
299+
// vectorized decode (cf. CRoaring bitset_extract_setbits_avx2/avx512).
299300
pos := i
300301
base := mask
301302
for k := 0; k < len(bc.bitmap); k++ {
302303
bitset := bc.bitmap[k]
303304
for bitset != 0 {
304-
t := bitset & -bitset
305-
x[pos] = base + uint32(bits.OnesCount64(t-1))
305+
x[pos] = base + uint32(bits.TrailingZeros64(bitset))
306306
pos++
307-
bitset ^= t
307+
bitset &= bitset - 1
308308
}
309309
base += 64
310310
}

bitmapcontainer_bench_test.go

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
package roaring
2+
3+
import (
4+
"math/rand"
5+
"testing"
6+
)
7+
8+
var sink uint32
9+
10+
func BenchmarkBitmapContainerFillLeastSignificant16bits(b *testing.B) {
11+
r := rand.New(rand.NewSource(42))
12+
bc := newBitmapContainer()
13+
for i := 0; i < 32768; i++ {
14+
val := uint16(r.Intn(65536))
15+
bc.iadd(val)
16+
}
17+
18+
x := make([]uint32, 65536)
19+
mask := uint32(123) << 16
20+
21+
b.ResetTimer()
22+
for i := 0; i < b.N; i++ {
23+
pos := bc.fillLeastSignificant16bits(x, 0, mask)
24+
sink += x[pos-1]
25+
}
26+
}

bitmapcontainer_test.go

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -532,3 +532,110 @@ func TestBitmapcontainerOrArrayCardinality(t *testing.T) {
532532
assert.Equal(t, 1024, result)
533533
})
534534
}
535+
536+
func TestBitmapContainerFillLeastSignificant16bitsProperties(t *testing.T) {
537+
runTest := func(t *testing.T, vals []uint16, mask uint32) {
538+
bc := newBitmapContainer()
539+
for _, val := range vals {
540+
bc.iadd(val)
541+
}
542+
543+
cardinality := len(vals)
544+
assert.Equal(t, cardinality, bc.getCardinality())
545+
546+
for _, startIdx := range []int{0, 13} {
547+
x := make([]uint32, startIdx+cardinality+10)
548+
// Fill x with a sentinel value to detect out-of-bound writes
549+
const sentinel = 0xDEADC0DE
550+
for j := range x {
551+
x[j] = sentinel
552+
}
553+
554+
pos := bc.fillLeastSignificant16bits(x, startIdx, mask)
555+
556+
// Assert return value matches container cardinality contract
557+
assert.Equal(t, startIdx+cardinality, pos)
558+
559+
// Assert prefix before startIdx is untouched
560+
for j := 0; j < startIdx; j++ {
561+
assert.Equal(t, uint32(sentinel), x[j])
562+
}
563+
564+
// Assert suffix after pos is untouched
565+
for j := pos; j < len(x); j++ {
566+
assert.Equal(t, uint32(sentinel), x[j])
567+
}
568+
569+
// Assert output contents and order
570+
for j, val := range vals {
571+
expected := mask + uint32(val)
572+
assert.Equal(t, expected, x[startIdx+j], "Mismatch at index %d for val %d", startIdx+j, val)
573+
}
574+
}
575+
}
576+
577+
t.Run("words >= 2 boundary and bits 63", func(t *testing.T) {
578+
// covers: words >= 2, bit 63 within words, word boundaries 0/63/64/127
579+
vals := []uint16{
580+
0, // boundary 0
581+
63, // word 0 bit 63
582+
64, // boundary 64
583+
127, // word 1 bit 63
584+
128, // word 2 boundary 0 (words >= 2)
585+
191, // word 2 bit 63
586+
255, // word 3 bit 63
587+
1024,
588+
1024 + 63,
589+
}
590+
runTest(t, vals, 0xFFFF0000)
591+
runTest(t, vals, 0x12340000)
592+
})
593+
594+
t.Run("full container 65536 bits at max mask 0xFFFF0000", func(t *testing.T) {
595+
vals := make([]uint16, 65536)
596+
for i := 0; i < 65536; i++ {
597+
vals[i] = uint16(i)
598+
}
599+
runTest(t, vals, 0xFFFF0000)
600+
})
601+
602+
t.Run("dense regime p0.95", func(t *testing.T) {
603+
r := rand.New(rand.NewSource(12345))
604+
vals := []uint16{}
605+
for i := 0; i < 65536; i++ {
606+
if r.Float64() < 0.95 {
607+
vals = append(vals, uint16(i))
608+
}
609+
}
610+
runTest(t, vals, 0xABCDE000)
611+
})
612+
613+
t.Run("sparse regime", func(t *testing.T) {
614+
r := rand.New(rand.NewSource(54321))
615+
vals := []uint16{}
616+
for i := 0; i < 65536; i++ {
617+
if r.Float64() < 0.01 {
618+
vals = append(vals, uint16(i))
619+
}
620+
}
621+
runTest(t, vals, 0x10000000)
622+
})
623+
624+
t.Run("random-word regimes", func(t *testing.T) {
625+
r := rand.New(rand.NewSource(999))
626+
vals := []uint16{}
627+
for word := 0; word < 1024; word++ {
628+
// 30% chance to populate this 64-bit word
629+
if r.Float64() < 0.3 {
630+
// Random word content
631+
w := r.Uint64()
632+
for bit := 0; bit < 64; bit++ {
633+
if (w & (1 << bit)) != 0 {
634+
vals = append(vals, uint16(word*64+bit))
635+
}
636+
}
637+
}
638+
}
639+
runTest(t, vals, 0x55550000)
640+
})
641+
}

0 commit comments

Comments
 (0)