一行“无用”的枚举反使Rust执行效率提升10%,编程到最后都是极致的艺术!

简介: 最近不少读者都留言说博客中的代码越来越反哺归真,但讨论的问题反倒越来越高大上了,从并发到乱序执行再到内存布局各种放飞自我。其实这倒不是什么放飞,只是Rust对我来说学习门槛太高了,学习过程中的挫败感也很强,在写完了之前的《Rust胖指针胖到底在哪》之后笔者一度决定脱坑Rust了,但截至本周这个目标还是没有实现,因为我所在的Rust学习群,有一个灵魂拷问,Rust的技术本质什么?不回答好这个问题,我简真是没法得到安宁。


最近不少读者都留言说博客中的代码越来越反哺归真,但讨论的问题反倒越来越高大上了,从并发到乱序执行再到内存布局各种放飞自我。

其实这倒不是什么放飞,只是Rust对我来说学习门槛太高了,学习过程中的挫败感也很强,在写完了之前的《Rust胖指针胖到底在哪》之后笔者一度决定脱坑Rust了,但截至本周这个目标还是没有实现,因为我所在的Rust学习群,有一个灵魂拷问,Rust的技术本质什么?不回答好这个问题,我简真是没法得到安宁。





Rust枚举的本质到底是什么?

1.枚举与一般变量定义的比较:首先说在枚举的处理上Rust与C/C++比较一致,从汇编的角度上看枚举和普通的变量声明的最大区别在于,枚举多存了一个类型的描述符。我们先来看下面的代码:

#[derive(Debug)]

enum IpAddr {

V4(u8, u8, u8, u8),

V6(String),

}

fn main(){

let a=127;

let b=0;

let c=0;

let d=1;

let home = IpAddr::V4(127, 0, 0, 1);


println!("{:#?}", home);

}

IP地址是枚举比较适合使用的场景,IP地址就是分为IPV6和IPV4两种细分类型,。与一般的结构体不同,IPV6与IPV4这两种类型是平等的关系,相互独立,非此即彼,而并非是IP类型下的两个元素,因此这时使用枚举类型IpAddr可以比较好的抽象IP地址这种场景。

将以上代码进行反汇编,可以看到与普通的变量定义与声明相比枚举对象的定义除了将相应的值存入栈以外,还会多存一个枚举的信息详见下图标红注释:



image.png



2.枚举与结构体的异同:我们还是以IP为例说明,IP地址分为V4与V6两大类型,不过单从IPV4的角度上看,如IP地址:127.0.0.1,其中每个网段,对于IPV4的址来说都其中的一部分,是共同组成的关系,这就比较适合使用结构的方式来进行定义,具体如下面的代码:


#[derive(Debug)]

enum IpAddr {

V4(u8, u8, u8, u8),

V6(String),

}

#[derive(Debug)]

struct IPV4(u8, u8, u8,u8);

fn main(){

   let a=127;

   let b=0;

   let c=0;

   let d=1;

   let home = IpAddr::V4(127, 0, 0, 1);

   let ipv4home= IPV4(a,b,c,d);

   let remotehost = IpAddr::V4(119, 3, 187, 35);

   let loopback = IpAddr::V6(String::from("::1"));

   let loopStr=String::from("::1");

   let remotehost1 = IpAddr::V6(String::from("1030::C9B4:FF12:48AA:1A2B"));    

   println!("{:#?}", home);

   println!("{:#?}", loopback);

   println!("{}",loopStr);

   println!("{:?}", remotehost);

   println!("{:?}", remotehost1);

    println!("{:?}",ipv4home);

}

将上述代码反汇编以后,可以看到与结构体相比,枚举也只是增加了一个枚举类型的记录。


image.png



综上所述我们基本可以把枚举当成一个基本类型的变量声明,只是编译器会通过0和1、2这种序号信息,记录下枚举对象具体是IPV4还是IPV6的信息仅此而已。当然一些细微的调整也会对编程范式造成革命性的进展,不过这些形而上的哲学思考,

注Rust反汇编与gdb调试的方案在前文《Rust胖指针到底胖在哪?》有详细介绍,其中反汇编的方法如下:

rustc -g rust源文件名.rs

objdump -S 编译后的文件名


一行无关代码,却让效率提高10%?

以上有关枚举的说明部分,比较容易理解,不过这不是今天的重点。

最近我所在的Rust学习群有不少同仁正在做一些并发和内存布局方面的研究,


image.png


我一顺手恰好将上面的代码实际上放在了一个Rust的并行原型程序中了,结果却意外发现执行时间缩短了5%-10%,我们刚刚也说了枚举类型与一般的变量定义区别不大,因此把代码简化后如下:

use std::thread;

fn main() {

      let mut s = String::with_capacity(100000000);


      let mut s1 = String::with_capacity(100000000);

      let handle = thread::spawn(move || {

          let mut i = 0;  

          while i < 10000000 {

             s.push_str("hello");

              i += 1;

           }

       });

          let handle1 = thread::spawn(move || {

          let mut i = 0;  

          while i < 10000000 {

             s1.push_str("hello");

              i += 1;

           }

       });

      handle.join().unwrap();

      handle1.join().unwrap();


}

上述代码的执行时间测试结果如下:

[root@ecs-a4d3 hello_world]# rustc  hello7.rs

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


real    0m0.999s

user    0m1.906s

sys     0m0.050s

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


real    0m1.093s

user    0m2.005s

sys     0m0.060s

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


real    0m1.079s

user    0m1.979s

sys     0m0.069s

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


real    0m1.011s

user    0m1.902s

sys     0m0.066s

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


real    0m1.031s

user    0m1.944s

sys     0m0.053s


但是在定义了一个无关的变量,并打印的步骤之后,代码如下:

use std::thread;

fn main() {

      let mut s = String::with_capacity(100000000);

      let reverbit="abcdefghijk";

      let mut s1 = String::with_capacity(100000000);

      let handle = thread::spawn(move || {

          let mut i = 0;  

          while i < 10000000 {

             s.push_str("hello");

              i += 1;

           }

       });

          let handle1 = thread::spawn(move || {

          let mut i = 0;  

          while i < 10000000 {

             s1.push_str("hello");

              i += 1;

           }

       });

      handle.join().unwrap();

      handle1.join().unwrap();

     println!("{}",reverbit);

}

在加了这个无关的变量定义之后,这段代码的执行时间和之前相比至少缩短了5%,这个成绩还是在多执行了print这个IO操作的基础上达到的,

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

abcdefghijk


real    0m0.963s

user    0m1.856s

sys     0m0.050s

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

abcdefghijk


real    0m0.960s

user    0m1.844s

sys     0m0.055s

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

abcdefghijk


real    0m0.964s

user    0m1.846s

sys     0m0.065s

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

abcdefghijk


real    0m0.958s

user    0m1.858s

sys     0m0.045s

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

abcdefghijk


real    0m0.963s

user    0m1.862s

sys     0m0.052s

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

abcdefghijk


real    0m0.963s

user    0m1.853s

sys     0m0.047s

在确认编译方法没有问题,之后我基本确认这个性能提升不是一个可以忽略的偶然事件。

前导小贴士初始化内存时尽量指定合适的容量:这段Rust程序其实就是通过两个线程handle、handle1分别去处理加工s、s1两个字符串,从程序本身来讲,只有一个小Tip要讲,就是初始化字符串的方式是通过 String::with_capacity方法来进行的,这里先回顾一下上次博客中所说的String内存布局。


image.png



在上面这个内存状态下,执行push_str("!")操作,字符串的capacity容量还没有溢出,不会向系统重新申请堆内存空间,也不会造成ptr指针的变化,只是将len+1,并在o后再加上!,完成后如下图:

image.png

也就是说提前将capacity容量设置成比较合适的大小,将避免反复向系统申请动态堆内存,提升程序运行效率。

无关代码提高效率的原因何在?


这里先给出的结论,这又是一个内存、缓存以及CPU多核之间的竞争协同效率问题。在分析这个问题之前我们还是要先回到上次博文中内容,其中String对象在栈上的三个成员ptr、capacity和len都是64位长,加在一起共192位也就是24byte,详见下图:



image.png




X86CPU的高速缓存每行容量却是64byte,也就是说按照我们最初的定义方式:

let mut s = String::with_capacity(100000000);

let mut s1 = String::with_capacity(100000000);

字符串s和s1占用连续的48byte栈空间,这种内存分配布局就使它们很可能位于同一个内存缓存行上,也就是说不同的CPU在分别操作s和s1时,其实操作的是同一缓存行,那么这样的操作就可能相互影响,从而使效率降低。

我们知道现代的CPU都配备了高速缓存,按照多核高速缓存同步的MESI协议约定,每个缓存行都有四个状态,分别是E(exclusive)、M(modified)、S(shared)、I(invalid),其中:

M:代表该缓存行中的内容被修改,并且该缓存行只被缓存在该CPU中。这个状态代表缓存行的数据和内存中的数据不同。


E:代表该缓存行对应内存中的内容只被该CPU缓存,其他CPU没有缓存该缓存对应内存行中的内容。这个状态的缓存行中的数据与内存的数据一致。

I:代表该缓存行中的内容无效。

S:该状态意味着数据不止存在本地CPU缓存中,还存在其它CPU的缓存中。这个状态的数据和内存中的数据也是一致的。不过只要有CPU修改该缓存行都会使该行状态变成 I 。

四种状态的状态转移图如下:

image.png


我们上文也提到了,在容量足够的情况下,执行执行push_str操作,并不会使程序向系统再次malloc内存,但是会使len的值有所变化,那么由于不同CPU在同时处理s1和s时其实是在操作同一缓存行,CPU0在操作s的len的同时CPU1很可能也在操作s1的len,这种remote write的操作,使该缓存行的状态总是会在S和I之间进行状态迁移,而一旦状态变为I将耗费比较多的时间进行状态同步。


image.png


因此我们可以基本得出let reverbit="abcdefghijk";这行无关的代码之后,改变了栈上的内存空间布局,无意中使s1和s被划分到了不同的缓存行上,这也使最终的执行效率有所提高。当然由于dump高速缓存的状态将从很大程度上改变程序的行为,因此本文的求证过程不像前几篇那么严谨,如有错漏还请各位读者指正。


这行看似啥用没有的let reverbit="abcdefghijk";代码最终却使效率提升了近10%,这也让人不得不感叹编程到了最后绝对是一门艺术,闲棋与闲子反而最显功力。

相关实践学习
2分钟自动化部署人生模拟器
本场景将带你借助云效流水线Flow实现人生模拟器小游戏的自动化部署
7天玩转云服务器
云服务器ECS(Elastic Compute Service)是一种弹性可伸缩的计算服务,可降低 IT 成本,提升运维效率。本课程手把手带你了解ECS、掌握基本操作、动手实操快照管理、镜像管理等。了解产品详情:&nbsp;https://www.aliyun.com/product/ecs
相关文章
|
4月前
|
Rust 安全 Go
揭秘Rust语言:为何它能让你在编程江湖中,既安全驰骋又高效超车,颠覆你的编程世界观!
【8月更文挑战第31天】Rust 是一门新兴的系统级编程语言,以其卓越的安全性、高性能和强大的并发能力著称。它通过独特的所有权和借用检查机制解决了内存安全问题,使开发者既能享受 C/C++ 的性能,又能避免常见的内存错误。Rust 支持零成本抽象,确保高级抽象不牺牲性能,同时提供模块化和并发编程支持,适用于系统应用、嵌入式设备及网络服务等多种场景。从简单的 “Hello World” 程序到复杂的系统开发,Rust 正逐渐成为现代软件开发的热门选择。
76 1
|
1月前
|
Rust 安全 区块链
探索Rust语言:系统编程的新选择
【10月更文挑战第27天】Rust语言以其安全性、性能和并发性在系统编程领域受到广泛关注。本文介绍了Rust的核心特性,如内存安全、高性能和强大的并发模型,以及开发技巧和实用工具,展示了Rust如何改变系统编程的面貌,并展望了其在WebAssembly、区块链和嵌入式系统等领域的未来应用。
|
2月前
|
Rust 算法 安全
如何学习Rust编程?
【10月更文挑战第12天】如何学习Rust编程?
56 1
|
2月前
|
Rust 安全 Java
探索Rust在系统级编程中的应用
【10月更文挑战第9天】Rust语言以其现代化设计、安全性和高性能,在系统级编程领域逐渐崭露头角。本文探讨Rust在操作系统开发、设备驱动、嵌入式系统和网络编程中的应用,介绍其核心优势及实施步骤,帮助读者了解如何在项目中有效利用Rust。
|
2月前
|
Rust 安全 Java
探索Rust在系统编程中的崛起
Rust 是一种由 Mozilla 研究院开发的现代系统编程语言,以其在安全性、并发性和内存管理方面的优势,逐渐成为开发者的新宠。Rust 提供内存安全保证且性能媲美 C/C++,支持跨平台开发,并具备强大的并发编程工具。本文将介绍 Rust 的核心优势、工作原理及实施方法,探讨其在系统编程中的崛起及其面临的挑战。尽管 Rust 学习曲线较陡,但其广泛的应用场景和不断壮大的社区使其成为构建高性能、安全应用的理想选择。
|
3月前
|
Rust 网络协议 安全
Rust在系统编程中的案例分享
Rust在系统编程中的案例分享
72 10
|
3月前
|
Rust 安全 前端开发
30天拿下Rust之图形编程
30天拿下Rust之图形编程
53 0
|
4月前
|
Rust 开发者
揭秘Rust编程:模块与包的终极对决,谁将主宰代码组织的新秩序?
【8月更文挑战第31天】在软件工程中,模块化设计能显著提升代码的可读性、可维护性和可重用性。Rust 作为现代系统编程语言,其模块和包管理机制为开发者提供了强有力的工具来组织代码。本文通过对比模块和包的概念及使用场景,探讨了 Rust 中的最佳实践。
45 2
|
4月前
|
Rust 安全 JavaScript
探索Rust在系统编程领域的前景:虚拟机和编译器开发的新篇章
【8月更文挑战第31天】在系统编程领域,性能与安全性至关重要。Rust作为一种新兴语言,凭借其独特的内存安全和并发特性,正逐渐成为虚拟机和编译器开发的首选。本文通过案例分析,探讨Rust在这些领域的应用,例如Facebook的Compiler VM (CVM)项目和实验性的JavaScript JIT编译器Mithril。Rust的静态类型系统和所有权模型确保了高性能和安全性,而其强大的包管理和库生态则简化了虚拟机的开发。随着Rust社区的不断成熟,预计未来将有更多基于Rust的创新项目涌现,推动系统编程的发展。对于追求高性能和安全性的开发者而言,掌握Rust将成为一个重要战略方向。
90 1
|
4月前
|
Rust 安全 物联网
解锁物联网安全新纪元!Rust如何悄然革新系统级编程,让智能设备“零风险”连接未来?
【8月更文挑战第31天】随着物联网(IoT)技术的发展,设备安全与效率成为关键挑战。Rust语言凭借其内存安全、高性能和并发优势,逐渐成为物联网开发的新宠。本文通过智能门锁案例,展示Rust如何确保生物识别数据的安全传输,并高效处理多用户请求。Rust的应用不仅限于智能家居,还广泛用于工业自动化和智慧城市等领域,为物联网开发带来革命性变化。
118 1