Files
dualfroz d648d30d86 Use the accelerated Count on AVX-512 CPUs
The AVX-512 level fell through to the generic loop. _count only needs
POPCNT, which every AVX-512 CPU has.

Fixes #46
2026-10-02 16:28:14 +02:00

718 lines
16 KiB
Go

// Copyright (c) Roman Atachiants and contributors. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for details.
package bitmap
import (
"fmt"
"math"
"strconv"
"testing"
"github.com/stretchr/testify/assert"
)
/*
cpu: 13th Gen Intel(R) Core(TM) i7-13700K
BenchmarkBitmap/set-24 655739137 1.803 ns/op 0 B/op 0 allocs/op
BenchmarkBitmap/remove-24 1000000000 1.107 ns/op 0 B/op 0 allocs/op
BenchmarkBitmap/contains-24 1000000000 0.8975 ns/op 0 B/op 0 allocs/op
BenchmarkBitmap/clear-24 827574 1487 ns/op 0 B/op 0 allocs/op
BenchmarkBitmap/ones-24 571444 2088 ns/op 0 B/op 0 allocs/op
BenchmarkBitmap/min-24 979591036 1.252 ns/op 0 B/op 0 allocs/op
BenchmarkBitmap/max-24 944884120 1.229 ns/op 0 B/op 0 allocs/op
BenchmarkBitmap/min-zero-24 991736356 1.258 ns/op 0 B/op 0 allocs/op
BenchmarkBitmap/max-zero-24 1000000000 1.157 ns/op 0 B/op 0 allocs/op
BenchmarkBitmap/count-24 393440 3086 ns/op 0 B/op 0 allocs/op
BenchmarkBitmap/count-to-24 58537441 20.20 ns/op 0 B/op 0 allocs/op
BenchmarkBitmap/clone-24 648651 1875 ns/op 0 B/op 0 allocs/op
BenchmarkBitmap/and-24 685710 1733 ns/op 0 B/op 0 allocs/op
BenchmarkBitmap/andnot-24 705882 1709 ns/op 0 B/op 0 allocs/op
BenchmarkBitmap/or-24 705894 1702 ns/op 0 B/op 0 allocs/op
BenchmarkBitmap/xor-24 705919 1721 ns/op 0 B/op 0 allocs/op
*/
func BenchmarkBitmap(b *testing.B) {
other := make(Bitmap, 1000000/64)
other.Set(1000000)
run(b, "set", func(index Bitmap) {
index.Set(5000)
})
run(b, "remove", func(index Bitmap) {
index.Remove(5000)
})
run(b, "contains", func(index Bitmap) {
index.Contains(5000)
})
run(b, "clear", func(index Bitmap) {
index.Clear()
})
run(b, "ones", func(index Bitmap) {
index.Ones()
})
run(b, "min", func(index Bitmap) {
index.Min()
})
run(b, "max", func(index Bitmap) {
index.Max()
})
run(b, "min-zero", func(index Bitmap) {
index.MinZero()
})
run(b, "max-zero", func(index Bitmap) {
index.MaxZero()
})
run(b, "count", func(index Bitmap) {
index.Count()
})
run(b, "count-to", func(index Bitmap) {
index.CountTo(5001)
})
into := make(Bitmap, len(other))
run(b, "clone", func(index Bitmap) {
index.Clone(&into)
})
run(b, "and", func(index Bitmap) {
index.And(other)
})
run(b, "andnot", func(index Bitmap) {
index.AndNot(other)
})
run(b, "or", func(index Bitmap) {
index.AndNot(other)
})
run(b, "xor", func(index Bitmap) {
index.AndNot(other)
})
}
/*
cpu: 13th Gen Intel(R) Core(TM) i7-13700K
BenchmarkMany/and4-noasm-24 66297 18139 ns/op 0 B/op 0 allocs/op
BenchmarkMany/and4-naive-24 179106 6803 ns/op 0 B/op 0 allocs/op
BenchmarkMany/and4-batch-24 258091 4679 ns/op 32 B/op 1 allocs/op
*/
func BenchmarkMany(b *testing.B) {
other := make(Bitmap, 1000000/64)
other.Set(1000000)
run(b, "and4-noasm", func(index Bitmap) {
max := minlen(index, other, nil)
index.shrink(max)
and(index, max, other, nil)
and(index, max, other, nil)
and(index, max, other, nil)
and(index, max, other, nil)
})
run(b, "and4-naive", func(index Bitmap) {
index.And(other)
index.And(other)
index.And(other)
index.And(other)
})
run(b, "and4-batch", func(index Bitmap) {
index.And(other, other, other, other)
})
run(b, "or4-batch", func(index Bitmap) {
index.Or(other, other, other, other)
})
run(b, "xor4-batch", func(index Bitmap) {
index.Xor(other, other, other, other)
})
shorter := make(Bitmap, 1000000/64/2)
shorter.Set(1000000 / 2)
run(b, "or4-batch-ragged", func(index Bitmap) {
index.Or(other, other, shorter, other)
})
run(b, "xor4-batch-ragged", func(index Bitmap) {
index.Xor(other, other, shorter, other)
})
}
func TestSetRemove(t *testing.T) {
index := Bitmap{}
for i := uint32(100); i < 200; i++ {
index.Set(i)
assert.True(t, index.Contains(i))
}
for i := uint32(150); i < 180; i++ {
index.Remove(i)
assert.False(t, index.Contains(i))
}
}
func TestClear(t *testing.T) {
index := Bitmap{}
for i := uint32(0); i < 500; i++ {
index.Set(i)
assert.True(t, index.Contains(i))
}
index.Clear()
index.Set(500)
for i := uint32(0); i < 500; i++ {
assert.False(t, index.Contains(i), i)
}
assert.True(t, index.Contains(500))
}
func TestAnd(t *testing.T) {
a, b := Bitmap{}, Bitmap{}
for i := uint32(0); i < 100; i += 2 {
a.Set(i)
b.Set(i)
}
a.And(b)
assert.False(t, a.Contains(1))
for i := uint32(0); i < 100; i += 2 {
assert.True(t, a.Contains(i))
}
}
func TestAndNot(t *testing.T) {
a, b := Bitmap{}, Bitmap{}
for i := uint32(0); i < 100; i += 2 {
a.Set(i)
b.Set(i)
}
a.AndNot(b)
assert.False(t, a.Contains(1))
for i := uint32(0); i < 100; i += 2 {
assert.False(t, a.Contains(i))
}
}
func TestAndNot_TheSameBitmap(t *testing.T) {
var a Bitmap
for i := uint32(0); i < 100; i += 2 {
a.Set(i)
}
a.AndNot(a)
for i := uint32(0); i < 100; i++ {
assert.Equal(t, false, a.Contains(i), "for "+strconv.Itoa(int(i)))
}
assert.Equal(t, 0, a.Count())
}
func TestAndNot_DifferentBitmapSizes(t *testing.T) {
var a, b, c, d Bitmap
for i := uint32(0); i < 100; i += 2 {
a.Set(i)
c.Set(i)
}
for i := uint32(0); i < 200; i += 2 {
b.Set(i)
d.Set(i)
}
a.AndNot(b)
d.AndNot(c)
for i := uint32(0); i < 100; i++ {
assert.Equal(t, false, a.Contains(i), "for "+strconv.Itoa(int(i)))
assert.Equal(t, false, d.Contains(i), "for "+strconv.Itoa(int(i)))
}
for i := uint32(100); i < 200; i++ {
assert.Equal(t, b.Contains(i), d.Contains(i), "for "+strconv.Itoa(int(i)))
}
assert.Equal(t, 0, a.Count())
assert.Equal(t, 50, d.Count())
}
func TestOr(t *testing.T) {
a, b := Bitmap{}, Bitmap{}
for i := uint32(0); i < 100; i += 2 {
b.Set(i)
}
a.Or(b)
assert.False(t, a.Contains(1))
for i := uint32(0); i < 100; i += 2 {
assert.True(t, a.Contains(i))
}
}
func TestOr_DifferentBitmapSizes(t *testing.T) {
var a, b, c, d Bitmap
for i := uint32(0); i < 100; i += 2 {
a.Set(i)
c.Set(i)
}
for i := uint32(0); i < 200; i += 2 {
b.Set(i)
d.Set(i)
}
a.Or(b)
d.Or(c)
for i := uint32(0); i < 200; i++ {
assert.Equal(t, d.Contains(i), a.Contains(i), "for "+strconv.Itoa(int(i)))
}
assert.Equal(t, 100, a.Count())
assert.Equal(t, 100, d.Count())
}
func TestXor(t *testing.T) {
a, b := Bitmap{}, Bitmap{}
for i := uint32(0); i < 100; i += 2 {
b.Set(i)
}
a.Xor(b)
assert.False(t, a.Contains(1))
for i := uint32(0); i < 100; i += 2 {
assert.True(t, a.Contains(i))
}
}
func TestXOr_DifferentBitmapSizes(t *testing.T) {
var a, b, c, d Bitmap
for i := uint32(0); i < 100; i += 2 {
a.Set(i)
c.Set(i)
}
for i := uint32(0); i < 200; i += 2 {
b.Set(i)
d.Set(i)
}
a.Xor(b)
d.Xor(c)
for i := uint32(0); i < 200; i++ {
assert.Equal(t, d.Contains(i), a.Contains(i), "for "+strconv.Itoa(int(i)))
}
assert.Equal(t, 50, a.Count())
assert.Equal(t, 50, d.Count())
}
func TestMin(t *testing.T) {
{
a := Bitmap{0x0, 0x0, 0xffffffffffffff00}
v, ok := a.Min()
assert.True(t, ok)
assert.Equal(t, 64+64+8, int(v))
assert.False(t, a.Contains(v-1))
assert.True(t, a.Contains(v))
}
{
a := Bitmap{0x0, 0x0}
v, ok := a.Min()
assert.False(t, ok)
assert.Equal(t, 0, int(v))
}
}
func TestMax(t *testing.T) {
{
a := Bitmap{0x0, 0x0, 0x00000000000000f0}
v, ok := a.Max()
assert.True(t, ok)
assert.Equal(t, 64+64+7, int(v))
assert.False(t, a.Contains(v-4))
assert.True(t, a.Contains(v-3))
assert.True(t, a.Contains(v-2))
assert.True(t, a.Contains(v-1))
assert.True(t, a.Contains(v))
assert.False(t, a.Contains(v+1))
assert.False(t, a.Contains(v+2))
}
{
a := Bitmap{0x0, 0x0}
v, ok := a.Max()
assert.False(t, ok)
assert.Equal(t, 0, int(v))
}
}
func TestMinZero(t *testing.T) {
{
a := Bitmap{0xffffffffffffffff, 0xffffffffffffffff, 0xf0ffffffffffff0f}
v, ok := a.MinZero()
assert.True(t, ok)
assert.Equal(t, 64+64+4, int(v))
assert.False(t, a.Contains(v))
}
{
a := Bitmap{0xffffffffffffffff, 0xffffffffffffffff}
v, ok := a.MinZero()
assert.False(t, ok)
assert.Equal(t, 0, int(v))
}
}
func TestMaxZero(t *testing.T) {
{
a := Bitmap{0xffffffffffffffff, 0xffffffffffffffff, 0xffffffffffffff0f}
v, ok := a.MaxZero()
assert.True(t, ok)
assert.Equal(t, 64+64+7, int(v))
assert.False(t, a.Contains(v))
}
{
a := Bitmap{0xffffffffffffffff, 0xffffffffffffffff}
v, ok := a.MaxZero()
assert.False(t, ok)
assert.Equal(t, 0, int(v))
}
}
func TestCount(t *testing.T) {
a := Bitmap{}
assert.Equal(t, 0, a.Count())
assert.Equal(t, 0, a.CountTo(math.MaxUint32))
b := Bitmap{}
b.Set(1)
b.Set(2)
b.Set(5)
b.Set(6)
b.Set(101)
b.Set(102)
b.Set(105)
b.Set(106)
assert.Equal(t, 8, b.Count())
assert.Equal(t, 1, b.CountTo(2))
assert.Equal(t, 2, b.CountTo(4))
assert.Equal(t, 4, b.CountTo(100))
assert.Equal(t, 4, b.CountTo(101))
assert.Equal(t, 5, b.CountTo(102))
assert.Equal(t, 8, b.CountTo(128))
assert.Equal(t, 8, b.CountTo(math.MaxUint32))
b.Set(127)
assert.Equal(t, 9, b.CountTo(128))
}
func TestCountWithHardware(t *testing.T) {
defer func(level int) { hardware = level }(hardware)
var b Bitmap
for i := uint32(0); i < 10000; i += 3 {
b.Set(i)
}
for _, level := range []int{isUnsupported, isAccelerated, isAVX512} {
hardware = level
assert.Equal(t, 3334, b.Count(), "hardware=%d", level)
}
}
func TestGrow(t *testing.T) {
bitmap := make(Bitmap, 1, 5)
bitmap[0] = 42
assert.Equal(t, 1, len(bitmap))
assert.Equal(t, 5, cap(bitmap))
assert.Equal(t, Bitmap{42}, bitmap)
bitmap.grow(0)
assert.Equal(t, 1, len(bitmap))
assert.Equal(t, 5, cap(bitmap))
assert.Equal(t, Bitmap{42}, bitmap)
bitmap.grow(4)
assert.Equal(t, 5, len(bitmap))
assert.Equal(t, 5, cap(bitmap))
assert.Equal(t, Bitmap{42, 0, 0, 0, 0}, bitmap)
bitmap.grow(5)
assert.Equal(t, 6, len(bitmap))
assert.Equal(t, Bitmap{42, 0, 0, 0, 0, 0}, bitmap)
bitmap.Grow(6)
}
func TestAnd_DifferentBitmapSizes(t *testing.T) {
var a, b, c, d Bitmap
for i := uint32(0); i < 100; i += 2 {
a.Set(i)
c.Set(i)
}
for i := uint32(0); i < 200; i += 2 {
b.Set(i)
d.Set(i)
}
a.And(b)
d.And(c)
for i := uint32(0); i < 200; i++ {
assert.Equal(t, a.Contains(i), d.Contains(i), "for "+strconv.Itoa(int(i)))
}
assert.Equal(t, 50, a.Count())
assert.Equal(t, 50, d.Count())
}
func TestAnd_ConsecutiveAnd_DifferentBitmapSizes(t *testing.T) {
var a, b, c Bitmap
for i := uint32(0); i < 200; i += 2 {
a.Set(i)
c.Set(i)
}
for i := uint32(0); i < 100; i += 2 {
b.Set(i)
}
a.And(b)
a.And(c)
for i := uint32(0); i < 200; i++ {
assert.Equal(t, a.Contains(i), b.Contains(i), "for "+strconv.Itoa(int(i)))
}
assert.Equal(t, 50, a.Count())
}
func TestResizeBitmap(t *testing.T) {
assert.Equal(t, 1, resize(100, 0))
assert.Equal(t, 2, resize(100, 1))
assert.Equal(t, 4, resize(100, 2))
assert.Equal(t, 16, resize(100, 11))
assert.Equal(t, 256, resize(100, 255))
assert.Equal(t, 1232, resize(100, 1000))
assert.Equal(t, 1232, resize(200, 1000))
assert.Equal(t, 1232, resize(512, 1000))
assert.Equal(t, 1213, resize(500, 1000)) // Inconsistent
assert.Equal(t, 22504, resize(512, 20000))
assert.Equal(t, 28322, resize(22504, 22600))
}
func TestMinInteger(t *testing.T) {
tests := [][3]int{
{10, 20, 10},
{20, 10, 10},
{0, 10, 0},
{10, 0, 0},
{10, 10, 10},
{10, -10, -10},
{-10, 10, -10},
{-10, 0, -10},
{-10, -10, -10},
}
for _, tc := range tests {
assert.Equal(t, tc[2], minint(tc[0], tc[1]), fmt.Sprintf("min(%v, %v)", tc[0], tc[1]))
}
}
func TestMaxInteger(t *testing.T) {
tests := [][3]int{
{10, 20, 20},
{20, 10, 20},
{0, 10, 10},
{10, 0, 10},
{10, 10, 10},
{10, -10, 10},
{-10, 10, 10},
{-10, 0, 0},
{-10, -10, -10},
}
for _, tc := range tests {
assert.Equal(t, tc[2], maxint(tc[0], tc[1]), fmt.Sprintf("max(%v, %v)", tc[0], tc[1]))
}
}
func TestBatched(t *testing.T) {
const bits = 0b0011
// Functions to test
tests := []func(Bitmap) func(Bitmap, ...Bitmap){
func(b Bitmap) func(Bitmap, ...Bitmap) {
return b.And
},
func(b Bitmap) func(Bitmap, ...Bitmap) {
return b.AndNot
},
func(b Bitmap) func(Bitmap, ...Bitmap) {
return b.Or
},
func(b Bitmap) func(Bitmap, ...Bitmap) {
return b.Xor
},
}
for _, withHw := range []int{isAccelerated, isUnsupported} {
for i, tc := range tests {
t.Run(fmt.Sprintf("%v,avx=%v", i, withHw), func(t *testing.T) {
hardware = withHw
naive := func(n int) Bitmap {
input := Bitmap{bits}
tc(input)(Bitmap{bits})
for i := 0; i < n; i++ {
tc(input)(Bitmap{bits})
}
return input
}
for n := 0; n < 5; n++ {
input := Bitmap{bits}
other := Bitmap{bits}
extra := make([]Bitmap, 0, n)
for i := 0; i < n; i++ {
extra = append(extra, Bitmap{bits})
}
tc(input)(other, extra...)
assert.Equal(t, naive(n), input)
}
})
}
}
}
func TestEmptyAnd(t *testing.T) {
var a, b Bitmap
a.And(b)
assert.Equal(t, 0, a.Count())
}
func TestEmptyAndNot(t *testing.T) {
var a, b Bitmap
a.AndNot(b)
assert.Equal(t, 0, a.Count())
}
func TestEmptyOr(t *testing.T) {
var a, b Bitmap
a.Or(b)
assert.Equal(t, 0, a.Count())
}
func TestEmptyXor(t *testing.T) {
var a, b Bitmap
a.Xor(b)
assert.Equal(t, 0, a.Count())
}
func TestTruthTables_NoSIMD(t *testing.T) {
hardware = isUnsupported
testTruthTables(t)
}
func TestTruthTables_SIMD(t *testing.T) {
hardware = isAccelerated
testTruthTables(t)
}
func testTruthTables(t *testing.T) {
{ // AND
a := Bitmap{0b0011, 0b1011, 0b1100, 0b0000, 0b0011, 0b1011, 0b1100, 0b0000, 0b0011}
a.And(Bitmap{0b0101, 0b1101, 0b1010, 0b1111, 0b0101, 0b1101, 0b1010, 0b1111, 0b0101})
assert.Equal(t, 0b0001, int(a[0]))
assert.Equal(t, 0b1001, int(a[1]))
assert.Equal(t, 0b1000, int(a[2]))
assert.Equal(t, 0b0000, int(a[3]))
assert.Equal(t, 0b0001, int(a[4]))
assert.Equal(t, 0b1001, int(a[5]))
assert.Equal(t, 0b1000, int(a[6]))
assert.Equal(t, 0b0000, int(a[7]))
assert.Equal(t, 0b0001, int(a[8]))
}
{ // AND NOT
a := Bitmap{0b0011, 0, 0, 0}
a.AndNot(Bitmap{0b0101})
assert.Equal(t, 0b0010, int(a[0]))
}
{ // OR
a := Bitmap{0b0011, 0, 0, 0}
a.Or(Bitmap{0b0101})
assert.Equal(t, 0b0111, int(a[0]))
}
{ // XOR
a := Bitmap{0b0011, 0, 0, 0}
a.Xor(Bitmap{0b0101})
assert.Equal(t, 0b0110, int(a[0]))
}
}
func TestOrRaggedSizes(t *testing.T) {
// Ragged inputs: every set bit must survive regardless of which is longest.
var dst Bitmap
dst.Set(1) // word 0
dst.Set(70) // word 1
var other Bitmap
other.Set(3) // word 0
other.Set(200) // word 3, beyond every extra bitmap
var short Bitmap
short.Set(5) // word 0 only
var mid Bitmap
mid.Set(130) // word 2
var long Bitmap
long.Set(260) // word 4, beyond other
dst.Or(other, short, mid, long)
want := []uint32{1, 70, 3, 200, 5, 130, 260}
for _, v := range want {
assert.True(t, dst.Contains(v), "missing bit "+strconv.Itoa(int(v)))
}
assert.Equal(t, len(want), dst.Count())
}
func TestXorRaggedSizes(t *testing.T) {
var dst Bitmap
dst.Set(1)
dst.Set(200) // shared with other, must cancel out
var other Bitmap
other.Set(200) // cancels dst's bit 200
other.Set(201) // survives, beyond every extra bitmap
var short Bitmap
short.Set(5)
var long Bitmap
long.Set(260) // word 4, beyond other
dst.Xor(other, short, long)
assert.True(t, dst.Contains(1))
assert.False(t, dst.Contains(200), "bit 200 should cancel")
assert.True(t, dst.Contains(201), "bit 201 from longer source must survive")
assert.True(t, dst.Contains(5))
assert.True(t, dst.Contains(260), "bit 260 from longer extra must survive")
assert.Equal(t, 4, dst.Count())
}