刷空间留言软件源码 Rust的本质到底是什么?与结构体的区别

制作 | CSDN博客

最近很多读者评论说,博客里的代码越来越原始,但讨论的问题却越来越高级,从并发到乱序执行再到内存布局。

其实这不是发布,只是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 地址这个场景。

反汇编上面的代码可以看到,与普通的变量定义和声明相比,枚举对象的定义不仅存储了栈上对应的值,还存储了额外的枚举信息。请参阅下面的图标。红色注释:

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);}

对上面的代码进行反汇编可以看出,与结构体相比,枚举只是增加了一个枚举类型的记录。

一行不相关的代码提高效率10%?

上面关于枚举的描述比较容易理解,但这不是今天的重点。最近我的 Rust 学习小组的很多同事都在做并发和内存布局方面的一些研究。

刚好把上面的代码放到了一个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 ./hello7real 0m0.999suser 0m1.906ssys 0m0.050s[root@ecs-a4d3 hello_world]# time ./hello7real 0m1.093suser 0m2.005ssys 0m0.060s[root@ecs-a4d3 hello_world]# time ./hello7real 0m1.079suser 0m1.979ssys 0m0.069s[root@ecs-a4d3 hello_world]# time ./hello7real 0m1.011suser 0m1.902ssys 0m0.066s[root@ecs-a4d3 hello_world]# time ./hello7real 0m1.031suser 0m1.944ssys 0m0.053s

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

图片[1]-刷空间留言软件源码 Rust的本质到底是什么?与结构体的区别-唐朝资源网

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 more的IO操作的基础上实现的。

[root@ecs-a4d3 hello_world]# time ./hello7abcdefghijkreal 0m0.963suser 0m1.856ssys 0m0.050s[root@ecs-a4d3 hello_world]# time ./hello7abcdefghijkreal 0m0.960suser 0m1.844ssys 0m0.055s[root@ecs-a4d3 hello_world]# time ./hello7abcdefghijkreal 0m0.964suser 0m1.846ssys 0m0.065s[root@ecs-a4d3 hello_world]# time ./hello7abcdefghijkreal 0m0.958suser 0m1.858ssys 0m0.045s[root@ecs-a4d3 hello_world]# time ./hello7abcdefghijkreal 0m0.963suser 0m1.862ssys 0m0.052s[root@ecs-a4d3 hello_world]# time ./hello7abcdefghijkreal 0m0.963suser 0m1.853ssys 0m0.047s

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

初步提示在初始化内存时,尽量指定合适的容量:这个Rust程序实际上分别通过两个线程handle和handle1来处理两个字符串s和s1。从程序本身来说,只有一个小窍门,就是说初始化字符串的方式是通过String::with_capacity方法。在这里,我们回顾一下上一篇博客中提到的String内存布局。

在上述内存状态下,执行了push_str(“!”)操作,字符串的容量没有溢出,堆内存空间不会被重新申请给系统,ptr指针也不会被改变了。 +1,并在 o! ,如下图:

也就是说,提前将容量设置为合适的大小,可以避免重复向系统申请动态堆内存,提高程序运行效率。

是什么让无关代码更有效率?

这里给出的结论是内存、缓存和CPU多核之间的另一个竞争和协同问题。在分析这个问题之前,我们还是要回到上一篇博文的内容。栈上String对象的三个成员ptr、capacity和len都是64位长,加起来就是192位或者24字节。详情见下文:

X86CPU的缓存容量是每行64bytes,也就是说按照我们原来的定义:

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

字符串 s 和 s1 占用连续的 48 字节堆栈空间。这种内存分配布局使得它们很可能位于同一个内存缓存行上,也就是说,当不同的CPU分别操作s和s1时,它们实际上操作的是同一个缓存行刷空间留言软件源码,那么这样的操作可能会相互影响,从而减少效率。

我们知道现代 CPU 配备了缓存。根据多核缓存同步的MESI协议,每个缓存行有四种状态,分别是E(独占)、M(修改)、S(共享)、I(无效)刷空间留言软件源码,其中:

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

我们上面也提到,当容量足够的时候,执行push_str操作不会导致程序再次malloc内存给系统,而是会让len的值发生变化,那么因为不同的CPU同时在处理当 s1 和 s 实际操作同一个 cache line 时,CPU0 很可能操作 s1 的 len 而 CPU0 操作 s 的 len。这种远程写操作使得cache line的状态始终在S和I之间,在它们之间进行状态转换,一旦状态变为I,就需要更多的时间来同步状态。

所以我们基本上可以得出结论 let reverbit=”abcdefghijk”;在这行不相关的代码之后,栈上的内存空间布局发生了变化,s1和s不经意间被分成了不同的缓存行,这也使得最终的执行效率得到了提升。当然,由于dump缓存的状态会极大地改变程序的行为,所以本文的验证过程没有前面那么严谨。如有错误或遗漏,请指正。

let reverbit=”abcdefghijk”这行,好像没什么用;代码最终提高了近10%的效率,这也让人不得不感叹编程到底是一门艺术。相反,它是最强大的。

马超,CSDN博客专家,阿里云MVP,华为云MVP,华为2020技术社区开发者之星。

© 版权声明
THE END
喜欢就支持一下吧
点赞96赞赏 分享
评论 抢沙发
头像
欢迎您留下宝贵的见解!
提交
头像

昵称

取消
昵称表情代码图片

    暂无评论内容