I’m hacking around with platformer mechanics in Ebitengine, a minimalist 2D game engine for Go. A friend recently asked me about the bitmasks I was using in the sourcecode for collision detection, so here’s a quick write-up.
Bitmasks
A unsigned 16-bit integer is declared like this.
var foo uint16 // 0000 0000 0000 0000
The comment shows the associated (stack) memory, in half-byte chunks. We can treat a uint16
as a compact bundle of 16
boolean values.
Binary constants
We can declare foo
explicitly by using the fact that any constant value starting with 0b
in Go represents a binary
integer.
var foo uint16 = 0b1100_1001_0110_0011
Go allows us to include the underscores to make large constant values easier to read. They’re ignored by the parser.
Printing in binary
fmt.Printf
allows us to examine the binary contents of any integer using the "%b"
formatting flag:
fmt.Printf("%b", 0b1011_1001) // prints "10111001"
Setting values
To set the nth value, (indexed from 0 to 15):
foo = foo | (1 << n)
That <<
is the “left-shift” bit operator. x << y
pads the binary representation of x
with y
zeroes on the right,
truncating any leftovers to fit in the available storage. For example:
0b1110110 << 4 == 0b01100000
The |
operator is bitwise boolean ‘OR’. It combines two integers by lining their binary representation up into columns
and ORing their contents. If we were in elementary school, we might do it “on paper” like below:
1100 0011 0110 0101
| 0000 0100 1111 0110
-----------------------
1100 0111 1111 0111
Putting the expression foo | (1<<n)
together, we can see that (1<<n)
constructs a value whose nth bit is set. ORing
that value with foo
sets the nth bit.
Testing values
How do we test whether the nth bit in a bitvector has been set? We can use boolean AND and compare the result to zero.
if foo & (1 << n) > 0 {
fmt.Println("bit %d is set", n)
}
Similarly to |
, the &
operator acts bitwise on integers as if each of their aligned pairs of bits were boolean values.
1100 0011 0110 0101
& 0000 0100 1111 0110
-----------------------
0000 0000 0110 0100
Since (1<<n)
is a value with only one bit set (the nth bit), ANDing that expression with foo
will only yield a
non-zero value if the nth bit of foo
is non-zero.
Unsetting values
Without mucking around with sign bits and two’s complement, I will just tell you that the following expression can be used to unset the nth bit.
foo = foo - (1<<n)
The reason this works involves sign bits and carrying, but you can see it in action for yourself.
fmt.Printf("%.8b", byte(0b00001011 - (1<<3))) // prints "00000011" -- %.8b" left pads with zeroes up to 8 bits
Testing multiple values at once
It’s not too hard to see how to set multiple values at once. If we set up some consts, we can use this to cheaply pass around small cardinality sets.
type SetOfThings uint8
const (
Nothing SetOfThings = 0
First = 1 // 0b0000_0001
Second = 2 // 0b0000_0010
Third = 4 // 0b0000_0100
Fourth = 8 // 0b0000_1000
)
Then the set containing the first and third things is First|Third
. We can check if a set contains the Second
thing
using set&Second > 0
, remove the Third
thing with set = set-Third
, and add the First
thing with
set = set|First
.
In fact, we can use iota
to manage the const block for us, instead of making everything so explicit. The constants in
the below block are identical to the ones above.
const (
Nothing SetOfThings = 0
First = 1 << (iota-1)
Second
Third
Fourth
)
The advantage of bitmasks over sets is that they can be tested cheaply in the CPU without accessing heap memory. The disadvantage is they only work for small cardinality sets. For large cardinality sets we can use a bitvector.
Bitvectors
A bitvector is just a []byte
which is treated like a large contiguous bitmask.
To access the nth bit, we first integer-divide 8 to find the correct byte, then take the modulus by 8 to find the location of the bit within the byte we found.
func Bitn(bitvector []byte, n int) bool { // returns true if bit n is set; false otherwise.
return bitvector[n/8] & (1<<(n%8)) > 0
}
Here’s an example for a bitvector with 40 bits. I’ve printed each byte in binary. If we’re looking for the 23rd bit, we
go to the 2nd index (23/8 = 2
), and look at the 7th bit (23%8 == 7
)
index 0 index 1 index 2 index 3 index 4
[ 0000_0000, 0100_0010, 0000_1000, 0101_0000, 0110_0000 ]
^--- 7th bit (0-indexed)
WARNING: The above function panics if the bitvector isn’t large enough to index with its argument. This is easily fixed.
With access in order, the rest of the code is simple, building on what we already know about bitmasks.
Setting values
Setting values is just the correct combination of bitvector access and the corresponding bitmask operation.
func Bitset(bitvector []byte, n int) {
bitvector[n/8] |= (1<<(n%8)) // |= is 'bitwise or equals', analogous to +=
}
Unsetting values
The same goes for unsetting values, combining access and the correct bitmask operation from above.
func Bitunset(bitvector []byte, n int) {
bitvector[n/8] -= (1<<(n%8))
}
Equality checks
Testing whether the entirety of one bitvector is equal to another is simple: use the standard library:
bytes.Equal
does exactly this.
Conclusion
Other operations are possible, but setting, unsetting, and testing are usually all that you need, if you need it. When
storing and comparing massive amounts of booleans, bitvectors can provide a significant space savings over a []bool
,
but as always, you should consider your use case carefully and evaluate whether the added complexity is worth the
benefits.
For more advanced bit-twiddling hacks, check out Sean Eron Anderson’s Website at Stanford.