专栏首页Rust学习专栏Rust为什么放弃Switch结构
原创

Rust为什么放弃Switch结构

今天我们还是继续来聊高并发的话题,我们知道Swich分支是一个非常有用的语法,这是一个可以回溯到上世纪的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 <main.main+0xa7>

                case 3:

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

  49a107:       75 0e                   jne    49a117 <main.main+0x137>

                        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 <main.main+0xa7>

                case 4:

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

  49a11b:       75 0e                   jne    49a12b <main.main+0x14b>

所以可以看出这个效率差完全是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主流哈希表比较》一文中有过介绍了,这里不再赘述。

原创声明,本文系作者授权云+社区发表,未经许可,不得转载。

如有侵权,请联系 yunjia_community@tencent.com 删除。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • 为什么要放弃 JSP ?

    随着时代的发展,渐渐的许多大中小公司开始把前后端的界限分的越来越明确,前端工程师只负责前端的事情,后端工程师只管后端的事情。正所谓术业有专攻,一个人如果什么都会...

    乔戈里
  • 为什么要放弃 JSP ?

    随着时代的发展,渐渐的许多大中小公司开始把前后端的界限分的越来越明确,前端工程师只负责前端的事情,后端工程师只管后端的事情。正所谓术业有专攻,一个人如果什么都会...

    Bug开发工程师
  • 为什么要放弃 Lombok ?

    如果您正在阅读此文,想必您对Project Lombok已经有了一段时间的了解。您是否正准备拥抱Lombok?还是正准备将如此酷炫的项目推荐给你的团队?如果您准...

    xcbeyond
  • 为什么要放弃 JSP ?

    随着时代的发展,渐渐的许多大中小公司开始把前后端的界限分的越来越明确,前端工程师只负责前端的事情,后端工程师只管后端的事情。正所谓术业有专攻,一个人如果什么都会...

    江南一点雨
  • 为什么要放弃 JSP ?

    随着时代的发展,渐渐的许多大中小公司开始把前后端的界限分的越来越明确,前端工程师只负责前端的事情,后端工程师只管后端的事情。正所谓术业有专攻,一个人如果什么都会...

    一个优秀的废人
  • 我为什么放弃了Chrome?

    导读:曾经,Firefox 和 Chrome 的出现,带领 Web 打破了微软 IE 浏览器竖起的高墙。而今,高喊“不作恶”的 Chrome 似乎正有意另起一座...

    华章科技
  • 为什么放弃Kafka,选择Pulsar?

    Pulsar 是 Yahoo 在 2013 年创建的,2016年贡献给了 Apache 基金会,目前已经是 Apache 的顶级项目。Yahoo、Verizon...

    用户7676729
  • Bowery为什么放弃Node.js,转向Go?

    英文来自:Moving from Node.js to Go at Bowery   Go 语言自从面世就受到了很多开发者的欢迎,越来越多的项目基于 Go 语言...

    李海彬
  • Java Web项目为什么要放弃JSP

    随着时代的发展,渐渐的许多大中小公司开始把前后端的界限分的越来越明确,前端工程师只负责前端的事情,后端工程师只管后端的事情。正所谓术业有专攻,一个人如果什么都会...

    秃头哥编程
  • 为什么我劝你放弃了Restful API?

    REST作为一种现代网络应用非常流行的软件架构风格,自从Roy Fielding博士在2000年他的博士论文中提出来到现在已经有了20年的历史。它的简单易用性,...

    开发者技术前线
  • Notion为什么能让我放弃手账

    今天在逛今日热榜[1]的时候,看到了一篇知乎的文章“我的最后一款笔记应用-Notion”[2],是种草notion的。

    scarsu
  • 为什么我们放弃了微服务?

    最近我们开发团队在开发计划中有一个小停顿,技术部门认为现在是将应用从单体架构迁移到微服务的最佳时机。经过一个月的准备和调查,我们取消了迁移,仍然使用单体模式。对...

    Bug开发工程师
  • 放弃微服务,我们为什么重回单体架构?

    InVision 公司的技术架构经历了从微服务合并回单体架构的过程,本文具体分析了这种架构迁移的技术原因和组织原因。

    深度学习与Python
  • 从入门到放弃心得 | 我为什么弃用GAN?

    苏黎世理工大学的博士Andreas Lugmayr历数了各种GAN的“罪状”,说出了今后弃用GAN的话。

    OpenCV学堂
  • 我用 Rust 重写了博客,谈谈我眼中的 Rust.

    我已经学习 Rust 有挺长的一段时间了,一直想用 Rust 来写点东西,但是缺乏好的想法; 有些想法自我感觉良好,但是又没有能力实现 (比如,写一个操作系统?...

    春哥大魔王
  • 154 - 杂谈 - 为什么我打算放弃Listary

    最近我给用了一段时间的uTools续费了。此外我也打算逐渐放弃Listary了。今天就和你聊聊这个事。

    用户1335799
  • 为什么我使用 GraphQL 而放弃 REST API?

    本文最初发布于 Max Desiatov 的个人博客,经原作者授权由 InfoQ 中文站翻译并分享。

    winty
  • 深度 | 从概念到实践,我们该如何构建自动微分库

    机器之心
  • 从概念到实践,我们该如何构建自动微分库

    选自Medium 作者:Maciej Kula 机器之心编译 参与:程耀彤、蒋思源 像 PyTorch 或 TensorFlow 这样通用的自动微分框架是非常有...

    企鹅号小编

扫码关注云+社区

领取腾讯云代金券