Golang 位运算实战用法

Golang 位运算实战用法一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第18天,点击查看活动详情。 开篇 之前在准备 Golang Mutex 原理解析 的时候,感觉在位运算的部分还是有一些比较经典的用法

一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第18天,点击查看活动详情

开篇

之前在准备 Golang Mutex 原理解析 的时候,感觉在位运算的部分还是有一些比较经典的用法可以分享的。今天用这篇文章,聊一下位运算的实战用法。

我个人不太喜欢讲位运算的时候,拿出来老三套,比如下面这样:

&      位运算 AND
|      位运算 OR
^      位运算 XOR
&^     位清空 (AND NOT)
<<     左移
>>     右移

然后拿出一堆 10110111 这样的二进制表示来说明就完事。每次看到这样的文章,心里总是五味杂陈。很多时候我们并不需要一个教程,教我们那些逻辑运算的布尔值,而是告诉我们,到底,我能怎样用位运算来帮助自己的日常开发。

今天这篇会着重在【实用性】,位运算本身是可以折腾出来很多奇技淫巧的,性能不错,但可读性极差。所以判断一个位运算的用法是否要掌握,个人感觉最关键的还是看你有没有可能真正在生产环境用到。

基础用法

如果你需要看一个数字的二进制表示,使用%b 打印即可。如 fmt.Printf("%b", 123) 将会得到 1111011

  • & 与运算,两个位都是 1 时,结果才为 1,否则为 0
  • | 或运算,两个位都是 0 时,结果才为 0,否则为 1
  • ^ 异或运算,两个位相同则为 0,不同则为 1
  • ~ 取反运算,0 则变为 1,1 则变为 0
  • << 左移运算,向左进行移位操作,高位丢弃,低位补 0
  • >> 右移运算,向右进行移位操作,对无符号数,高位补 0,对于有符号数,高位补符号位
A   = 0011 1100
B   = 0000 1101
---------------

A&B = 0000 1100
A|B = 0011 1101
A^B = 0011 0001

实战用法

1. 左右移等价于 2 的 n 次方运算

记住:左乘右除

  • i << n 相当于 i 乘以 2 的 n次方;
  • i >> n 相当于 i 除以 2 的 n 次方
type ByteSize float64
const(
  B ByteSize = 1<<(10*iota)  // 1<<(10*0)
  KB  // 1<<(10*1) 左移动10位 2的10次方=1024
  MB  // 1<<(10*2)
  GB  // 1<<(10*3)
  TB  // 1<<(10*4)
  PB  // 1<<(10*5)
)

2. 判断奇偶数

奇偶数的本质区别就是看是否为 2 的倍数,直白点讲,在二进制的世界里,看最低位是 0 还是 1 即可。一个 & 就能解决问题:

  • 奇数:i&1 == 1
  • 偶数:i&1 == 0

3. 异或的特性

  • i ^ i == 0
  • i ^ 0 == i

两个相同的数异或的结果是 0,一个数和 0 异或的结果是它本身。

实际上,我们经常用的 staticcheck 就内置了这里的规则,如果你写出了类似 i ^ i 的代码,会得到 identical expressions on the left and right side of the '^' operator (SA4000)

4. 枚举

很多时候我们会针对一个主体,会存入多个状态。比如一篇掘金博客,你可以对它进行【点赞】【收藏】【评论】三种操作。那么当我们要去查看一篇指定的文章,当前用户做了哪些操作时。我们应该怎么存数据呢?

最直观的方案是,我维护三个变量:hasLike, hasCollect, hasComment。如果用户做了对应的操作,我就把那个 bool 值改成 true。

有了位运算,你会发现,这样的开销是不必要的。

位运算的精髓就在于,明明只是一个数,但因为你把它拆成了二进制里的一个个位,从此它具备了存储更多信息的能力。只需要将上面的每一种枚举错位开,就可以通过检查当前位的值,判断状态。

const (
	Like = 1 << iota
	Collect
	Comment
)

这里 Like 为 1,Collect 为 2,Comment 为 4。转为二进制以后分别为 001, 010, 100。作为 2 的 n次方,我们保证了只有一位是 1,其他都是 0。

这个前提的重要意义在于,当你按位去进行【或运算】时,不会导致二进制位上的 1 出现重叠。也就意味着,当我把它们【或】起来的时候,在一个最终的结果里,实际上包含了他们的每个标志位的信息。

func main() {
	ability := Like | Collect | Comment

	fmt.Printf("%b\n", ability)                 // 111

	fmt.Println((ability & Like) == Like)       // true
	fmt.Println((ability & Collect) == Collect) // true
	fmt.Println((ability & Comment) == Comment) // true
}

看代码就一目了然了,原来的 001, 010, 100 三个数字进行【或运算】之后,得到了 111 (按十进制的话是 7)。每一个原始为 1 的位,就是它们自己的标志位。

所以,当我们用 ability & Like 时,由于 Like 的非标志位都是 0,标志位是 1,也就意味着,若 ability 具备了 Like 的标志位能力,则这一次 & 过后,结果必然和 Like 是完全相等的。

这就是为什么,当我们将三个枚举都【或】进 ability 时,再去【与】操作,会发现相等。

假如我们稍微做一点改动,ability 不把 Collect 或进去了,你会发现结果也变了:

func main() {
	ability := Like | Comment

	fmt.Printf("%b\n", ability)                 // 101

	fmt.Println((ability & Like) == Like)       // true
	fmt.Println((ability & Collect) == Collect) // false
	fmt.Println((ability & Comment) == Comment) // true
}

ok,或进去了,就代表有能力了。那我如果想把某个能力下掉呢?比如原来 ability 是同时包含了 Like, Comment,现在这个用户不喜欢这篇文章了,把点赞去掉了,那我 ability 应该怎么搞?

思考一下,此时 aibility 应该是 111,而 Like 是 001,我的目标是把 ability 变成 110 (这样就去掉了 Like 对应的标志位)。

111
001
---
110

发现了么?这不就是【异或】嘛。若位相同则为0,若不同则为 1。 此时我们只需要执行一次

ability = ability ^ Like

或者用简洁的写法:

ability ^= Like

这样就够了。总结一下:

  • 定义 int 枚举值时注意二进制错位,每个枚举有一个自己的标志位;
  • 用 | 操作添加能力;
  • 用 ^ 操作下掉能力;
  • 用 & 操作校验是否具备对应的能力。

完整的示例代码如下:

package main

import "fmt"

const (
	Like = 1 << iota     // 1的0次方,1
	Collect              // 2
	Comment              // 4
)

func main() {
	ability := Like | Comment

	fmt.Printf("%b\n", ability)                    // 101

	fmt.Println((ability & Like) == Like)          // true
	fmt.Println((ability & Collect) == Collect)    // false
	fmt.Println((ability & Comment) == Comment)    // true

	ability = ability ^ Like
	fmt.Printf("%b\n", ability)                    // 100
	fmt.Println((ability & Like) == Like)          // false
}

案例:Mutex state 的设计

对 Mutex 不熟悉的同学可以参考上一篇文章 Golang Mutex 原理解析

这一节,我们直接看看 Golang 官方的 Mutex 库是怎样落地我们在上一节提到的用法的。

state 从语义上来说包含了三个状态(Locked, Woken, Starving),和 Waiter 的数量。

它的类型是个 int32,这里运用了位操作来存储信息。这一节我们来看看相关的逻辑。

mutex1.png

const (
	mutexLocked = 1 << iota // mutex is locked
	mutexWoken
	mutexStarving
	mutexWaiterShift = iota
)

将 iota 表达式解析为二进制:

mutexLocked: 001
mutexWoken: 010
mutexStarving: 100

mutexWaiterShift: 011

有没有发现,mutexWaiterShift 是个异类,它的定位和三种状态不同。

state 是 Mutex 的一个成员变量,它的值代表了 Mutex 的状态。在等待加锁的 lockSlow 函数中,我们看到了这样的用法:

old := m.state // 此处获取到锁的状态

if old&(mutexLocked|mutexStarving) == mutexLocked && runtime_canSpin(iter) {
        // Active spinning makes sense.
        // Try to set mutexWoken flag to inform Unlock
        // to not wake other blocked goroutines.
        if !awoke && old&mutexWoken == 0 && old>>mutexWaiterShift != 0 &&
                atomic.CompareAndSwapInt32(&m.state, old, old|mutexWoken) {
                awoke = true
        }
        runtime_doSpin()
        iter++
        old = m.state
        continue
}

这个 old&(mutexLocked|mutexStarving) == mutexLocked 很有意思,我们前面说过,要校验能力可以采用 & 操作。但前面的示例只给的需要校验单一能力的场景。那这里先【或】再跟 old 去【与】,是什么含义呢?

mutexLocked|mutexStarving 等价于 001 | 100,也就是 101。

而要做到 old & 101 的结果等于 mutexLocked(也就是 001),那么 old 只能为 001 或者 011。也就是说,中间的一位不重要,前后两位必须是 0 和 1,即:

  1. 有 mutexLocked 的标志位
  2. 没有 mutexStarving 的标志位

所以,old&(mutexLocked|mutexStarving) == mutexLocked 的含义就是,需要有 mutexLocked 的能力,但是不能有 mutexStarving 的能力。

那如果是 old&(mutexLocked|mutexStarving|mutexWoken) == mutexLocked 呢?也就是 old & 111 == 001,那么 old 只能是 001 了。也就是说,需要有 mutexLocked 的能力,但是不能有 mutexStarving 的能力,也不能有 mutexWoken 的能力。

这样就能做到对于多个状态的判断。

还有一点比较有意思,如果按照此前例子的写法,我们会用 old&mutexWoken == mutexWoken 来判断是否具有 mutexWoken 能力。但是在 lockSlow 中,官方采用了 old&mutexWoken == 0 来判断。

仔细想想,的确如此。我们定义错位的常量时的前提是,只能有一个标志位,其他位都是 0。那么,若不具备这个标志位的能力,那么【与】的结果势必是 0。所以,针对这种场景,直接一个 & 操作,跟 0 比对即可。

只有一个标志位,意味着跟它进行与的结果,要么是自身,要么是 0。

old&mutexWoken == 0 代表没有能力 old&mutexWoken == mutexWoken 代表有能力

至于 mutexWaiterShift,其实 Mutex 并没有用到他的【位能力】,这里仅仅是把它当成 3 (因为三个状态用掉了低三位),用于左移右移操作而已。

一个 state int32 包含32位,低三位留出来作为标志位,本质是三个状态的 bool。高 29位留出来,存储被阻塞的 waiter(其实是 goroutine) 的数量。

参考资料

Go中的bit位和位运算符

位运算有什么奇技淫巧

学习位运算

今天的文章Golang 位运算实战用法分享到此就结束了,感谢您的阅读。

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
如需转载请保留出处:https://bianchenghao.cn/16881.html

(0)
编程小号编程小号

相关推荐

发表回复

您的电子邮箱地址不会被公开。 必填项已用*标注