Deeply analyze the principle and application of go bit operation and bit mask

Posted by osti on Fri, 10 Dec 2021 02:44:23 +0100

As a non professional self-taught wild programmer, it's really silly to just come into contact with bit operation, but it's not so mysterious after being familiar with it. It's often used when watching some excellent open source projects or custom network protocol encapsulation. Recently, I saw a good article on bit operation by foreigners. So translate it. Casually expand their practical application in the project.

Original text: Using Bitmasks In Go

Note: my translation level is limited. If there are errors, please correct them

When we write a multiplayer online server similar to a role game MMORPG , in the game, players will collect a large number of keys. How to design to store these keys for each player?

For example, imagine that these keys are copper, jade and crystal. We may consider the following method of storing keys

  • []string
  • map[string]bool

Both methods are effective, but have we considered the third method of using bit mask? Using bit mask will make storing and processing keys more efficient. Once you understand the principle, this method will also be easy to read and maintain.

Introduction to bit operation

First, understand how the computer stores numbers in 8-bit byte s. How to convert binary to decimal, and each bit is an integer power of 2

| 2⁷| 2⁶| 2⁵| 2⁴| 2³| 2²| 2¹| 2⁰|  <- Bit Position
|---|---|---|---|---|---|---|---|
|128| 64| 32| 16| 8 | 4 | 2 | 1 |  <- Base 10 Value

The rightmost bit represents 2 ⁰ (the lowest bit), 1 in hexadecimal, and the second bit represents 2 ¹ or 2), the leftmost bit represents 2 ⁷ or 128

For example, it represents the number 13. We split it with 8, 4 and 1. The following represents the result

| 0 | 0 | 0 | 0 | 1 | 1 | 0 | 1 |  <- Bit Position
|---|---|---|---|---|---|---|---|
|128| 64| 32| 16| 8 | 4 | 2 | 1 |  <- Base 10 Value
=================================
  0+  0+  0+  0+  8+  4+  0+  1    = 13
We can use% b to print the number Binary , FMT. Printf ('% 08B \ n', 13) will print 00001101, which means that the maximum value can only represent 255.
| 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 |  <- Bit Position
|---|---|---|---|---|---|---|---|
|128| 64| 32| 16| 8 | 4 | 2 | 1 |  <- Base 10 Value
=================================
 128+ 64+ 32+ 16+ 8+  4+  2+  1    = 255

AND (&)

If two digits are 1 at the same time, the result is 1

0 & 0 -> 0 (false)
0 & 1 -> 0 (false)
1 & 0 -> 0 (false)
1 & 1 -> 1 (true)

Example: 5 AND 3 do AND

00000101 AND  (4, 1)
00000011      (2, 1)
--------------------
00000001      (1)

OR (|)

As long as one bit is true, the result is true

0 | 0 -> 0 (false)
0 | 1 -> 1 (true)
1 | 0 -> 1 (true)
1 | 1 -> 1 (true)

Example: 5 and 3 do OR

00000101 OR  (4, 1)
00000011     (2, 1)
-------------------
00000111     (4, 2, 1)

NOT (^)

Reverse binary

^1 -> 0
^0 -> 1

<<

Shift left operator Means multiply by two

00001010 (10) << 1
------------------
00010100 (20)

>>

Shift right operator Represents divided by two

00010100 (20) >> 1
------------------
00001010 (10)

Using these operations, we can implement complex logic. In our case, we can set or cancel a bit, and check that a bit is not set

Back to our question

The application needs to support three key s. We only need three bits and only need to allocate one byte of memory. First, define a new type with type. The original type is byte, and byte and uint8 are equivalent in go.

type KeySet byte
11 const (
12     Copper  KeySet = 1 << iota // 1
13     Jade                       // 2
14     Crystal                    // 4
15     maxKey                     // 8
16 )

The above is the keys we support. We use iota to define the first enumeration. Next, the number of bits of iota + 1 will be automatically shifted to the left. In order to make our keys have better semantics when printing, we implement fmt.Stringer Interface

18 // String implements the fmt.Stringer interface
19 func (k KeySet) String() string {
20     if k >= maxKey {
21         return fmt.Sprintf("<unknown key: %d>", k)
22     }
23 
24     switch k {
25     case Copper:
26         return "copper"
27     case Jade:
28         return "jade"
29     case Crystal:
30         return "crystal"
31     }
32 
33     // multiple keys
34     var names []string
35     for key := Copper; key < maxKey; key <<= 1 {
36         if k&key != 0 {
37             names = append(names, key.String())
38         }
39     }
40     return strings.Join(names, "|")
41 }

Now define the {KeySet in our structure

43 // Player is a player in the game
44 type Player struct {
45     Name string
46     Keys KeySet
47 }

Line 45 defines the player name and line 46 defines these key s. More fields may be added when the game is developed

AddKey

49 // AddKey adds a key to the player keys
50 func (p *Player) AddKey(key KeySet) {
51     p.Keys |= key
52 }

The above shows how to add a key using a bit mask. In line 51, we use the bit operation OR and the passed KeySet to set the Keys field and pass it into Crystal.

p.Keys : 00000001 OR  (Copper)
key    : 00000100     (Crystal)
---------------------------------------
result : 00000101     (Copper, Crystal)

We can see that the result contains Crystal

HasKey

54 // HasKey returns true if player has a key
55 func (p *Player) HasKey(key KeySet) bool {
56     return p.Keys & key != 0
57 }

In line 56, we use the bitwise operation AND to check whether the passed key is in p.Keys. We can pass in Crystal AND operate through HasKey method AND and operator.

p.Keys : 00000101 AND  (Copper, Crystal)
key    : 00000100      (Crystal)
----------------------------------------
result : 00000100      (Crystal)

We can see that the result contains Crystal, so it is a match. When we check Jade, we can't find it

p.Keys : 00000101 AND  (Copper, Crystal)
key    : 00000010      (Jade)
----------------------------------------
result : 00000000      Nothing

RemoveKey

59 // RemoveKey removes key from player
60 func (p *Player) RemoveKey(key KeySet) {
61     p.Keys &= ^key
62 }

We first negate the key, And then reset the key with the And operator.

p.Keys : 00000101 AND  (Copper, Crystal)
^key   : 11111011      (org: 00000100 Crystal)
----------------------------------------
result : 00000001      (Copper)

We can see that the key of the result no longer contains Crystal

In practice, we can also see minus sign To empty, is this feasible? The following operation obtains the same result. The principle is constant Only the setting bit will be 1. If you subtract, you will only subtract this one and there will be no borrowing. In project development, if we want to maintain atomic operation, we can only use addition and subtraction instead of bit operation. The premise must be met. Similarly, addition can be used for setting.

Note: the premise of subtraction is that this is 1, Addition operation The premise is that this is 0, so judge whether the premise is met before use

fmt.Printf("%08b\n",0b00000101-0b00000100) //00000001

conclusion

  • go's type system allows you to combine lower level code bit operations with upper level function methods to write more elegant code
  • How about the performance of bit mask? See the following benchmark , the results of [] string, map[string]bool and byte are compared.
$ go test -bench . -benchmem
goos: linux
goarch: amd64
pkg: github.com/353words/bitmask
cpu: Intel(R) Core(TM) i7-7500U CPU @ 2.70GHz
BenchmarkMap-4          249177445            4.800 ns/op           0 B/op          0 allocs/op
BenchmarkSlice-4        243485120            4.901 ns/op           0 B/op          0 allocs/op
BenchmarkBits-4         1000000000           0.2898 ns/op          0 B/op          0 allocs/op
BenchmarkMemory-4       21515095            52.25 ns/op       32 B/op          1 allocs/op
PASS
ok      github.com/353words/bitmask 4.881s
  • From the results, the bit operation performance is more than 16 times that of the others. Use the - benchmem parameter to display the memory allocation. In the memory allocation, we can see that [] string{"copper", "jade"} consumes 32 bytes, which is 32 times that of a single byte of bit operation

expand

Determine whether it is an integer power of 2

principle Parity operation Just like taking the module, take 32 as an ex amp le. All bits of 32 - 1 are 1, except the highest bit, and only the highest bit of 32 is 1, so the result of & will be 0

0001 1111 (31) &
0010 0000 (32)
------------------      
0000 0000 (0)    

Take 16 as an example. For the same result, if this number is not an integer power of 2, then there must be a bit that is 1 and the result is not 0

0000 1111 (15) &
0001 0000 (16)
------------------      
0000 0000 (0)    

Therefore, the final calculated function is IsPowerTwo. Excluding 0 and negative numbers, 0 is also an integer power satisfying the condition but not 2

package main
​
import "fmt"
​
func main() {
    fmt.Println(IsPowerTwo(32))//true
    fmt.Println(IsPowerTwo(16))//true
    fmt.Println(IsPowerTwo(10))//false
    fmt.Println(IsPowerTwo(100))//false
    fmt.Println(IsPowerTwo(0))//false
}
func IsPowerTwo(num int) bool {
    return (num>0)&&(num&(num-1)==0)
}
​

Bit operation modulo

Different from conventional% mold taking, the following rules will be followed

  • The modulus to be taken must be an integer power of 2 - 1. For example, if it is 4, the modulus to be taken is 4-1 = 3

Principle:

First, let's look at the characteristics of the integer power of 2: one bit is 1

0000 0001 1
0000 0010 2
0000 0100 4
0000 1000 8
0001 0000 16
0010 0000 32

If Minus one The following result will be obtained. The result is that the highest bit is 0 and the other bits are 1

0000 0000 0
0000 0001 1
0000 0011 3
0000 0111 7
0000 1111 15
0001 1111 31

At this time, how to perform the AND operation AND what will be sent? Take 31 AND 8 as examples. Any number can be used here. Suppose it is data smaller than 31. Because 31 is 1, each bit of the number involved in the operation will get itself, which is the result for itself.

0001 1111 (31) &
0000 1000 (8)
------------------  =====>8%32=8
0000 1000 (8)

If it is a large data, such as 100, there is no more than 31 bits, so directly supplement 0. The final result is as follows:

0001 1111 (31) &
0110 0100 (100)
------------------      =====>100%32=4
0000 0100 (4)    

Application:

  • High efficiency mold taking
  • take Array length Locate the integer power of 2 and take the modulus as the index. At this time, taking the modulus of the integer power of 2 - 1 to get the index will not cross the boundary

mutex Median operation Use of

Lock status definition

var  state int32
const (
    mutexLocked = 1 << iota   1(1<<0)
    mutexWoken                2(1<<1)
    mutexStarving             4(1<<2)
    mutexWaiterShift = iota   8(1<<3)
)
  • Judge whether the status is mutexLocked only
state&(mutexLocked|mutexStarving) == mutexLocked
  • Judge whether the status contains mutexWoken
state&mutexWoken == 0
  • Judge whether mutexLocked and mutexstarting exist in the status
state&(mutexLocked|mutexStarving) != 0
  • Remove mutexWoken
state &^= mutexWoken  ==>state &=^mutexWoken =>state=state&(^mutexWoken)
  • Atomic operations can be performed on a series of values, including addition and subtraction. The following meaning means to add the value of mutexLocked to the number greater than - 1 represented by the mutexWaiterShift bit and remove the value of mutexstarting. Before addition, it is ensured that the mutexLocked does not exist, and - mutexstarting also adds a judgment to judge whether the number exists
delta := int32(mutexLocked - 1<<mutexWaiterShift)
delta -= mutexStarving
atomic.AddInt32(&m.state, delta)

Topics: Go Back-end