Skip to content

Commit 51791bd

Browse files
authored
Various improvements (#6)
* fix typo * Speedup Equals Using benchmarks, the threshold of 128 uint64 (1024 bits) is defined. Above it, bytes.Equal is used after unsafe casting slice header, this allows very interesting speedups: name old time/op new time/op delta Equals/len=100-8 5.30ns ±10% 5.31ns ± 0% ~ (p=0.429 n=6+5) Equals/len=500-8 8.28ns ± 6% 8.06ns ± 1% ~ (p=0.394 n=6+6) Equals/len=1000-8 11.5ns ± 1% 11.2ns ± 7% ~ (p=0.065 n=6+6) Equals/len=2000-8 18.4ns ± 2% 14.8ns ± 9% -19.62% (p=0.002 n=6+6) Equals/len=4000-8 32.6ns ± 2% 18.5ns ± 4% -43.13% (p=0.002 n=6+6) Equals/len=8000-8 67.8ns ± 3% 26.4ns ± 4% -60.99% (p=0.002 n=6+6) * Swap i and nbits in Uintn/Intn methods * Simplify integer tests * Improve examples * Cosmetics and comments * Inline naive comparison in u64cmp * EqualRange uses u64cmp * Improve test coverage * EqualRange: test all cases * Cosmetics * Update README.md * Cosmetics * Add Bitstring.CopyRange * Update README.md * Cosmetics * Cosmetics * bitops: add ispow2() * Generate reversed bits lookup table * Add Reverse * Cosmetics * Improve Bitstring.Big() * Factor rightShiftBits * Make Clone a method * Add LeadingZeroes and TrailingZeroes * Use math.MaxUint64 and 64 directly * Fix BigInt on 32-bit architectures * Move non-test helpers in helpers.go * Add lsb and msb functions * Add NewFromBig * Add NewFromBig example
1 parent 4cbd22a commit 51791bd

22 files changed

+1844
-1359
lines changed

README.md

Lines changed: 30 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -3,20 +3,36 @@
33
[![Go Report Card](https://goreportcard.com/badge/github.com/arl/bitstring)](https://goreportcard.com/report/github.com/arl/bitstring)
44
[![codecov](https://codecov.io/gh/arl/bitstring/branch/main/graph/badge.svg)](https://codecov.io/gh/arl/bitstring)
55

6-
# bitstring
6+
# `bitstring`
7+
78
Go bitstring library
89

9-
Package `bitstring` implements a fixed-length bit string type and many bit manipulation functions:
10-
- set/clear/flip a single bit
11-
- set/clear/flip a range of bits
12-
- swap or compare range of bits between 2 bitstrings
13-
- 8/16/32/64/n signed/unsigned to/from conversions
14-
- count ones/zeroes
15-
- gray code conversion methods
16-
- convert to `big.Int`
17-
- Copy/Clone methods
18-
19-
TODO:
20-
- RotateLeft/Right
10+
Package `bitstring` implements a fixed length bit string type and bit manipulation functions.
11+
12+
- Get/Set/Clear/Flip a single bit: `Bit`|`SetBit`|`ClearBit`|`FlipBit`
13+
- Set/Clear/Flip a range of bits: `SetRange`|`ClearRange`|`FlipRange`
14+
- Compare 2 bit strings: `Equals` or `EqualsRange`
15+
- 8/16/32/64/N signed/unsigned to/from conversions:
16+
- `Uint8`|`Uint16`|`Uint32`|`Uint64`|`Uintn`
17+
- `SetUint8`|`SetUint16`|`SetUint32`|`SetUint64`|`SetUintn`
18+
- Count ones/zeroes: `ZeroesCount`|`OnesCount`
19+
- Gray code conversion methods: `Gray8`|`Gray16`|`Gray32`|`Gray64`|`Grayn`
20+
- Convert to/from `big.Int`: `BigInt` | `NewFromBig`
21+
- Copy/Clone methods: `Copy`|`Clone`|`CopyRange`
22+
23+
24+
# Debug version
25+
26+
By default, bit offsets arguments to `bitstring` methods are not checked. This
27+
allows not to pay the performance penalty of always checking offsets, in
28+
environments where they are constants or always known beforehand.
29+
30+
You can enable runtime checks by passing the `bitstring_debug` build tag to `go`
31+
when building the `bitstring` package.
32+
33+
**TODO**:
34+
- RotateLeft/Right ShiftLeft/Right
2135
- Trailing/Leading zeroes/ones
22-
- improve documentation
36+
- Or, And, Xor between bitstrings
37+
- Reverse
38+
- Run CI on big|little endian and 32|64 bits (for now only amd64) (see https://github.com/docker/setup-qemu-action)

bench_test.go

Lines changed: 51 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ func benchmarkUintn(b *testing.B, nbits, i int) {
1212
bs, _ := NewFromString("0000000000000000000000000000000101000000000000000000000000000000")
1313
var v uint64
1414
for n := 0; n < b.N; n++ {
15-
v = bs.Uintn(nbits, i)
15+
v = bs.Uintn(i, nbits)
1616
}
1717
b.StopTimer()
1818
sink = v
@@ -102,7 +102,7 @@ func BenchmarkSwapRange(b *testing.B) {
102102
b.ResetTimer()
103103
b.ReportAllocs()
104104
for i := 0; i < b.N; i++ {
105-
SwapRange(x, y, 1, 1024)
105+
SwapRange(x, y, i%26, 1026)
106106
}
107107
}
108108

@@ -119,18 +119,27 @@ func BenchmarkRandom(b *testing.B) {
119119
sink = x
120120
}
121121

122-
func BenchmarkEquals(b *testing.B) {
123-
rng := rand.New(rand.NewSource(99))
124-
x := Random(1026, rng)
125-
y := Clone(x)
126-
var res bool
127-
b.ResetTimer()
128-
b.ReportAllocs()
129-
for i := 0; i < b.N; i++ {
130-
res = x.Equals(y)
122+
func benchmarkEquals(len int) func(b *testing.B) {
123+
return func(b *testing.B) {
124+
x, y := New(len), New(len)
125+
var res bool
126+
b.ResetTimer()
127+
b.ReportAllocs()
128+
for i := 0; i < b.N; i++ {
129+
res = x.Equals(y)
130+
}
131+
b.StopTimer()
132+
sink = res
131133
}
132-
b.StopTimer()
133-
sink = res
134+
}
135+
136+
func BenchmarkEquals(b *testing.B) {
137+
b.Run("len=100", benchmarkEquals(100))
138+
b.Run("len=500", benchmarkEquals(500))
139+
b.Run("len=1000", benchmarkEquals(1000))
140+
b.Run("len=2000", benchmarkEquals(2000))
141+
b.Run("len=4000", benchmarkEquals(4000))
142+
b.Run("len=8000", benchmarkEquals(8000))
134143
}
135144

136145
func BenchmarkSetUint8(b *testing.B) {
@@ -149,7 +158,7 @@ func BenchmarkSetUintn(b *testing.B) {
149158
b.ResetTimer()
150159
b.ReportAllocs()
151160
for i := 0; i < b.N; i++ {
152-
bs.SetUintn(64, 35, 0x9cfbeb71ee3fcf5f&uintsize)
161+
bs.SetUintn(35, 64, 0x9cfbeb71ee3fcf5f&64)
153162
}
154163
b.StopTimer()
155164
sink = bs
@@ -179,11 +188,36 @@ func benchmarkOnesCount(b *testing.B, val string) {
179188
sink = ones
180189
}
181190

182-
// var ones uint
183-
// b.ResetTimer()
184-
// b.ReportAllocs()
185191
func BenchmarkOnesCount(b *testing.B) {
186192
for _, tt := range vals {
187193
b.Run("", func(b *testing.B) { benchmarkOnesCount(b, tt) })
188194
}
189195
}
196+
197+
func Benchmark_msb(b *testing.B) {
198+
nums := []uint64{
199+
atobin("1100001000000000000000000000000100000000000000000000000000000000"),
200+
atobin("01"),
201+
atobin("10"),
202+
atobin("10000000000100000000000000000001"),
203+
atobin("110000000000100000000000000000001"),
204+
atobin("00000000010001111111000000000100"),
205+
atobin("00000001000001111111000000000000"),
206+
atobin("01000000000000000000000000000000"),
207+
atobin("1000000000000000000000000000000000000000000000000000000000000000"),
208+
atobin("0100000000000000000000000000000000000000000000000000000000000000"),
209+
atobin("1111111111111111111111111111111111111111111111111111111111111111"),
210+
atobin("1011111111111111111111111111111111111111111111111111111111111111"),
211+
atobin("1111111111111111111111111111111111111111111111111111111111111110"),
212+
}
213+
214+
b.ResetTimer()
215+
var val uint64
216+
for i := 0; i < b.N; i++ {
217+
for _, num := range nums {
218+
val = msb(num)
219+
}
220+
}
221+
222+
sink = val
223+
}

bitops.go

Lines changed: 127 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,60 +1,161 @@
11
package bitstring
22

3-
// bitmask returns a mask where only the nth bit of a uint is set.
3+
import (
4+
"math"
5+
"unsafe"
6+
)
7+
8+
const wordsize = 32 << (^uint(0) >> 63) // 32 or 64
9+
10+
// bitmask returns a mask where only the nth bit of a word is set.
411
func bitmask(n uint64) uint64 { return 1 << n }
512

613
// wordoffset returns, for a given bit n of a bit string, the offset
7-
// of the uint that contains bit n.
14+
// of the word that contains bit n.
815
func wordoffset(n uint64) uint64 { return n / 64 }
916

10-
// bitoffset returns, for a given bit n of a bit string, the offset of
11-
// that bit with regards to the first bit of the uint that contains it.
17+
// bitoffset returns, for a given bit n of a bit string, the offset of that bit
18+
// with respect to the first bit of the word that contains it.
1219
func bitoffset(n uint64) uint64 { return n & (64 - 1) }
1320

14-
// mask returns a mask that keeps the bits in the range [l, h) behaviour
21+
// mask returns a mask that keeps the bits in the range [l, h) behavior
1522
// undefined if any argument is greater than the size of a machine word.
1623
func mask(l, h uint64) uint64 { return lomask(h) & himask(l) }
1724

1825
// lomask returns a mask to keep the n LSB (least significant bits). Undefined
19-
// behaviour if n is greater than uintsize.
20-
func lomask(n uint64) uint64 { return maxuint >> (64 - n) }
26+
// behavior if n is greater than 64.
27+
func lomask(n uint64) uint64 { return math.MaxUint64 >> (64 - n) }
2128

2229
// himask returns a mask to keep the n MSB (most significant bits). Undefined
23-
// behaviour if n is greater than uintsize.
24-
func himask(n uint64) uint64 { return maxuint << n }
30+
// behavior if n is greater than 64.
31+
func himask(n uint64) uint64 { return math.MaxUint64 << n }
2532

26-
// firstSetBit returns the offset of the first set bit in w
27-
func firstSetBit(w uint64) uint64 {
33+
// transferbits returns the word that results from transferring some bits from
34+
// src to dst, where set bits in mask specify the bits to transfer.
35+
func transferbits(dst, src, mask uint64) uint64 {
36+
return dst&^mask | src&mask
37+
}
38+
39+
// lsb returns the offset of the lowest significant set bit in v. That is, the
40+
// index of the rightmost 1.
41+
//
42+
// Note: lsb(0) is meaningless, it's the caller responsibility to not use the
43+
// result of lsb(0).
44+
func lsb(v uint64) uint64 {
2845
var num uint64
2946

30-
if (w & 0xffffffff) == 0 {
47+
if (v & 0xffffffff) == 0 {
3148
num += 32
32-
w >>= 32
49+
v >>= 32
3350
}
34-
if (w & 0xffff) == 0 {
51+
if (v & 0xffff) == 0 {
3552
num += 16
36-
w >>= 16
53+
v >>= 16
3754
}
38-
if (w & 0xff) == 0 {
55+
if (v & 0xff) == 0 {
3956
num += 8
40-
w >>= 8
57+
v >>= 8
4158
}
42-
if (w & 0xf) == 0 {
59+
if (v & 0xf) == 0 {
4360
num += 4
44-
w >>= 4
61+
v >>= 4
4562
}
46-
if (w & 0x3) == 0 {
63+
if (v & 0x3) == 0 {
4764
num += 2
48-
w >>= 2
65+
v >>= 2
4966
}
50-
if (w & 0x1) == 0 {
67+
if (v & 0x1) == 0 {
5168
num++
5269
}
5370
return num
5471
}
5572

56-
// transferbits returns the uint that results from transferring some bits from
57-
// src to dst, where set bits in mask specify the bits to transfer.
58-
func transferbits(dst, src, mask uint64) uint64 {
59-
return dst&^mask | src&mask
73+
// msb returns the offset of the most significant set bit in v. That is, the
74+
// index of the leftmost 1.
75+
//
76+
// Note: msb(0) is meaningless, it's the caller responsibility to not use the
77+
// result of msb(0).
78+
func msb(v uint64) uint64 {
79+
var num uint64
80+
81+
if (v & 0xffffffff00000000) != 0 {
82+
num += 32
83+
v >>= 32
84+
}
85+
if (v & 0xffff0000) != 0 {
86+
num += 16
87+
v >>= 16
88+
}
89+
if (v & 0xff00) != 0 {
90+
num += 8
91+
v >>= 8
92+
}
93+
if (v & 0xf0) != 0 {
94+
num += 4
95+
v >>= 4
96+
}
97+
if (v & 0xc) != 0 {
98+
num += 2
99+
v >>= 2
100+
}
101+
if (v & 0x2) != 0 {
102+
num++
103+
v >>= 1
104+
}
105+
106+
return num
107+
}
108+
109+
// fastmsbLittleEndian is faster version of msb that only works on little endian
110+
// architectures. About 50% faster than msb on amd64. Rely on the fact that Go
111+
// uses IEEE 754 floating point representation. Converts v to float64, then
112+
// extracts the exponent bits of the IEEE754 representation.
113+
func fastmsbLittleEndian(v uint64) uint64 {
114+
if v == math.MaxUint64 {
115+
return 63
116+
}
117+
118+
f := float64(v)
119+
arr := *(*[2]uint32)(unsafe.Pointer(&f))
120+
return uint64(arr[1]>>20 - 1023)
121+
}
122+
123+
func reverseBytes(buf []byte) []byte {
124+
for i := 0; i < len(buf)/2; i++ {
125+
buf[i], buf[len(buf)-i-1] = buf[len(buf)-i-1], buf[i]
126+
}
127+
return buf
128+
}
129+
130+
// shifts all words of n bits to the right. invariant: 0 <= off <= 64.
131+
func rightShiftBits(words []uint64, n uint64) {
132+
shift := 64 - n
133+
mask := lomask(n)
134+
prev := uint64(0) // bits from previous word
135+
136+
for i := len(words) - 1; i >= 0; i-- {
137+
save := (words[i] & mask) << shift
138+
words[i] >>= n
139+
words[i] |= prev
140+
prev = save
141+
}
142+
}
143+
144+
// if n is a power of 2, ispow2 returns (v, true) such that (1<<v) gives n, or
145+
// (0, false) if n is not a power of 2.
146+
//
147+
// panics if n == 0
148+
func ispow2(n uint64) (uint64, bool) {
149+
if (n & -n) != n {
150+
// n is not a power of 2
151+
return 0, false
152+
}
153+
154+
for i := uint64(0); i < 64; i++ {
155+
if n == 1<<i {
156+
return i, true
157+
}
158+
}
159+
160+
panic("unreachable")
60161
}

0 commit comments

Comments
 (0)