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
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.| 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
| 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 (^)
^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)