为什么新一代的Rust、Go等编程语言都如此讨厌if-else、Switch结构

为什么新一代的Rust、Go等编程语言都如此讨厌if-else、Switch结构,第1张

今天我们还是继续来聊高并发的话题,我们知道Swich及if-else分支是一个非常有用的语法,这是一个可以回溯到上世纪的Pascal、C等经典语言的分支结构,主要的作用就是判断变量的取值并将程序代码送入不同的分支,这种设计在当时的环境下非常的精妙,但是在当前最新的CPU环境下,却会带来很多意想不到的坑。

Swich的坑,环境一变效率就差远了

由于Rust并没有Switch了,因此以下代码就暂用Go语言来演示了。我们先来看以下这段代码:

package main



import (

        "fmt"

        "math/rand"



        //"sync"

        "time"

)



func main() {

        now := time.Now().UnixNano()

        count := []int{0, 0, 0, 0, 0, 0}



        for i := 0; i < 100000; i++ {

                 random := rand.Intn(100)

                 switch random {

                 case 1:

                         count[1]++

                 case 2:

                         count[2]++

                 case 3:

                         count[3]++

                 case 4:

                         count[4]++

                 case 5:

                         count[5]++

                 default:

                         count[0]++

                 }

        }



        fmt.Println(time.Now().UnixNano() - now)

        fmt.Println(count)



}

它的运行结果如下:

[root@ecs-a4d3 hello_world]# time ./test1

2818084

[99507 86 108 106 96 97]



real  0m0.004s

user 0m0.003s

sys   0m0.001s

我们再稍微把Random的范围由100改为5,

package main



import (

        "fmt"

        "math/rand"



        //"sync"

        "time"

)



func main() {

        now := time.Now().UnixNano()

        count := []int{0, 0, 0, 0, 0, 0}



        for i := 0; i < 100000; i++ {

                 random := rand.Intn(1000)

                 switch random {

                 case 1:

                         count[1]++

                 case 2:

                         count[2]++

                 case 3:

                         count[3]++

                 case 4:

                         count[4]++

                 case 5:

                         count[5]++

                 default:

                         count[0]++

                 }

        }



        fmt.Println(time.Now().UnixNano() - now)

        fmt.Println(count)



}

上述的代码运行结果如下:

[root@ecs-a4d3 hello_world]# time ./test1

4365712

[20184 20357 19922 19897 19640 0]



real  0m0.006s

user 0m0.004s

sys   0m0.002s

可以看到这两段程序的运行时间相差了30%多,这个结果真的是细思极恐,因为也就是实际代码执行逻辑完全没有任何变化 ,只是变量取值范围有所调整,就会使程序的运行效率大为不同,也就是说当系统遭遇到某些极端情况时,同样的程序所需要的运行时间却有天壤之别,要解释这个问题要从指令流水线说起。

CPU流水线与指令预测

我们知道CPU的每个动作都需要用晶体震荡而触发,以加法ADD指令为例,想完成这个执行指令需要取指、译码、取 *** 作数、执行以及取 *** 作结果等若干步骤,而每个步骤都需要一次晶体震荡才能推进,因此在流水线技术出现之前执行一条指令至少需要5到6次晶体震荡周期才能完成

指令/时刻

T1

T2

T3

T4

T5

ADD

取指

译码

取 *** 作数

执行

取结果

为了缩短指令执行的晶体震荡周期,芯片设计人员参考了工厂流水线机制的提出了指令流水线的想法,由于取指、译码这些模块其实在芯片内部都是独立的,完成可以在同一时刻并发执行,那么只要将多条指令的不同步骤放在同一时刻执行,比如指令1取指,指令2译码,指令3取 *** 作数等等,就可以大幅提高CPU执行效率:

指令/时

T1

T2

T3

T4

T5

T6

T7

T8

指令1

取指

译码

取 *** 作数

执行

取结果 

指令2

取指

译码

取 *** 作数

执行

取结果 

指令3

取指

译码

取 *** 作数

执行

取结果 

指令4

取指

译码

取 *** 作数

执行

取结果 

指令5

取指

译码

取 *** 作数

执行

指令6

 

取指

译码

取 *** 作数

指令7

 

 

取指

译码

指令8

 

 

 

取指

以上图流水线为例 ,在T5时刻之前指令流水线以每周期一条的速度不断建立,在T5时代以后每个震荡周期,都可以有一条指令取结果,平均每条指令就只需要一个震荡周期就可以完成。这种流水线设计也就大幅提升了CPU的运算速度。

但是CPU流水线高度依赖指指令预测技术,假如在流水线上指令5本是不该执行的,但却在T6时刻已经拿到指令1的结果时才发现这个预测失败,那么指令5在流水线上将会化为无效的气泡,如果指令6到8全部和指令5有强关联而一并失效的话,那么整个流水线都需要重新建立。

指令/时刻

T1

T2

T3

T4

T5

T6

T7

T8

指令1

取指

译码

取 *** 作数

执行

取结果 

指令2

取指

译码

取 *** 作数

执行

取结果 

指令3

取指

译码

取 *** 作数

执行

取结果 

指令4

取指

译码

取 *** 作数

执行

取结果 

指令5

取指

译码

取 *** 作数

执行

指令6

 

取指

译码

取 *** 作数

指令7

 

 

取指

译码

指令8

 

 

 

取指

会是编译器隐式likely优化的原因吗?因此为了适应指令流水线的机制,不少对于性能要求极高的程序中,都会将不太会执行到的分支加上unlikely修饰符,从而引导CPU不要预测这个分支上的代码以提升效率,不过这个机制在本例中不太适用,会objdump –S查看原始代码也并没有发现,default前面加上了likely优化,

default:

                        count[0]++

  49a0f9:       48 8b 44 24 60          mov    0x60(%rsp),%rax

  49a0fe:       48 ff 00                incq   (%rax)

  49a101:       eb 84                   jmp    49a087 

                case 3:

  49a103:       48 83 f8 03             cmp    $0x3,%rax

  49a107:       75 0e                   jne    49a117 

                        count[3]++

  49a109:       48 8b 44 24 60          mov    0x60(%rsp),%rax

  49a10e:       48 ff 40 18             incq   0x18(%rax)

  49a112:       e9 70 ff ff ff          jmpq   49a087 

                case 4:

  49a117:       48 83 f8 04             cmp    $0x4,%rax

  49a11b:       75 0e                   jne    49a12b 

所以可以看出这个效率差完全是CPU指令预测造成的。也就是说CPU自带的机制就是会对于执行概比较高的分支给出更多的预测倾斜。

Rust的Elseif也是一样的坑

当然我们说switch不好也就不是说else if就避免了这个问题,根据指令流水线的原理,elseif在处理分支时情况也一样,因此Rust也不太推荐else if的写法,以Rust为例如下:

use rand::Rng;



fn main() {

  

let mut rng = rand::thread_rng();

let mut arr = [0, 0, 0, 0, 0,0];

//println!("Integer: {}", arr[random]);

for i in 0..1000000 {

let mut random =rng.gen_range(0,5);

if random==0{

arr[0]=arr[0]+1;

}else if random==1{

arr[1]=arr[1]+1;

}else if random==2{

arr[2]=arr[2]+1;

}else if random==3{

arr[3]=arr[3]+1;

}else if random==4{

arr[4]=arr[4]+1;

}else{

arr[5]=arr[5]+1;

}

}

println!("{},{},{},{},{},{}", arr[0], arr[1], arr[2], arr[3], arr[4], arr[5]);

}

将随机数的取值范围调整一下

let mut random =rng.gen_range(0,100);

可以观察到这两段程序运行的时间也要相差近40%,这样的结果也深刻说明了一个问题,这个例子其实模拟的是一个极端状态,也就是某一个变量取值突然从0-100变成了0-5那么程序的运行效率可能就会有极大的改变,这个推论就是一旦系统运行在某一个极端状态,比如错误占比过高或者其它极端场景,那么正常情况下的压力测试结果也许就完全不能说明问题了。

哈希表会是个好的选择吗?

我们上文也介绍过哈希表也就是字典,可以快速将键值key转化为值value,从某种程度上讲可以替换switch的作用,按照第一段代码的逻辑,用哈希表重写的方案如下:

package main



import (

        "fmt"

        "math/rand"

        "time"

)



func main() {

        now := time.Now().UnixNano()

        //count := []int{0, 0, 0, 0, 0, 0}

        count := map[int]int{0: 0, 1: 0, 2: 0, 3: 0, 4: 0, 5: 0}



        for i := 0; i < 100000; i++ {

                 random := rand.Intn(5)

                 count[random]++

        }

        fmt.Println(time.Now().UnixNano() - now)

        fmt.Println(count)



}

不过这段代码的运行结果如下:

[root@ecs-a4d3 hello_world]# time ./test2

5864780

map[0:20184 1:20357 2:19922 3:19897 4:19640 5:0]



real  0m0.008s

user 0m0.007s

sys   0m0.001s

虽然这个版本性能比较稳定,但却比之前的SWITCH方案最慢的情况还慢60%,原因也很简单我们之前介绍过哈希表也叫散列表,它的各个元素在内存中的而已并不连续,因此高速缓存对这种数据结构的加速作用有限。当然这点我们已经在《Go、Java、Rust主流哈希表比较》一文中有过介绍了,这里不再赘述。

欢迎分享,转载请注明来源:内存溢出

原文地址: http://outofmemory.cn/langs/995795.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-05-21
下一篇 2022-05-21

发表评论

登录后才能评论

评论列表(0条)

保存