专栏首页Rust学习专栏Go和Rust的高并发编程中,为什么要特别注意对齐?
原创

Go和Rust的高并发编程中,为什么要特别注意对齐?

从传统意义上讲,对齐是指将变量的存储按照计算机的字长进行边界对章,这里字长一般是指一个WORD的位数,也就是现代计算机中一次IO的数据处理长度,通过计算机的字长与CPU的寄存器长度相等。现代的CPU一般都不是按位进行内存访问,而是按照字长来访问内存,当CPU从内存或者磁盘中将读变量载入到寄存器时,每次操作的最小单位一般是取决于CPU的字长。比如8位字是1字节,那么至少由内存载入1字节也就是8位长的数据,再比如32位CPU每次就至少载入4字节数据, 64位系统8字节以此类推。

对齐详解

那么以8位机为例咱们来看一下这个问题。假如变量1是个bool类型的变量,它占用1位空间,而变量2为byte类型占用8位空间,假如程序目前要访问变量2那么,第一次读取CPU会从开始的0x00位置读取8位,也就是将bool型的变量1与byte型变量2的高7位全部读入内存,但是byte变量的最低位却没有被读进来,还需要第二次的读取才能把完整的变量2读入,详见下图:

也就是说变量的存储应该按照CPU的字长进行对齐,当访问的变量长度不足CPU字长的整数倍时,需要对变量的长度进行补齐。这样才能提升CPU与内存间的访问效率,避免额外的内存读取操作。

一般来说只要保证变量存储的首地址恰好是CPU字长的整数倍就能做到按照字长对齐了。这方面绝大多数编译器都做得很好,在缺省情况下,C编译器为每一个变量或是数据单元按其自然对界条件分配空间边界。也可以通过pragma pack(n)调用来改变缺省的对界条件指令,调用后C编译器将按照pack(n)中指定的n来进行n个字节的对齐,这其实也对应着汇编语言中的.align。以上这些工作现代的编译器都做得很好了。

我们可以来比较下面两段代码,由于我测试的平台是64位的机器,因此我选择的占位变量1是bool类型,变量2为int64类型,如果没有做对齐的话那么变量2在实际中需要读取两次,不过这些优化编译器和CPU都会帮你做好,以下两段代码的执行效率并没有明显不同。

代码段一有占位变量,

fn main() {

 let j=true;

 let mut i:u64=0;

while i < 100000000 {

i += 1

}

println!("{}", j);

println!("{}", i);

}

代码段二,无占位变量

fn main() {

 //let j=true;

 let mut i:u64=0;

while i < 100000000 {

i += 1

}

//println!("{}", j);

println!("{}", i);

}

按照缓存行对齐

在没有并发竞争的情况下,按照CPU字长进行对齐就完全可以了,但是如果在并发的情况下,即使没有共享变量,也可能会造成伪共享的问题,我们来看下面的代码,代码示例一中四个个goroutine分别操作slicea中的前四个元素,

package main



import (

"fmt"

"time"

)



func main() {



s1icea := []int64{0, 1, 2, 3, 4, 5, 6, 7}

//s1iceb := []int64{0, 1, 2, 3, 4, 5, 6, 7}

//s1icec := []int64{0, 1, 2, 3, 4, 5, 6, 7}

//s1iced := []int64{0, 1, 2, 3, 4, 5, 6, 7}

go func() {

for {

s1icea[0]++

}

}()

go func() {

for {

s1icea[1]++

}

}()

go func() {

for {

s1icea[2]++

}

}()

go func() {

for {

s1icea[3]++

}

}()



time.Sleep(time.Second)

fmt.Println(s1icea)

}

运行结果如下:

[269164771 265021684 258089104 267919418 4 5 6 7]

而代码示例二中两个goroutine分别操作slicea和sliceb,

package main



import (

"fmt"

"time"

)



func main() {



s1icea := []int64{0, 1, 2, 3, 4, 5, 6, 7}

s1iceb := []int64{0, 1, 2, 3, 4, 5, 6, 7}

s1icec := []int64{0, 1, 2, 3, 4, 5, 6, 7}

s1iced := []int64{0, 1, 2, 3, 4, 5, 6, 7}

go func() {

for {

s1icea[0]++

}

}()

go func() {

for {

s1iceb[1]++

}

}()

go func() {

for {

s1icec[2]++

}

}()

go func() {

for {

s1iced[3]++

}

}()



time.Sleep(time.Second)

fmt.Println(s1icea, s1iceb, s1icec, s1iced)

}

运行结果如下:

[399287607 1 2 3 4 5 6 7] [0 406576583 2 3 4 5 6 7] [0 1 403888391 3 4 5 6 7] [0 1 2 396400686 4 5 6 7]

这两段代码在我四核的机器上测试,性能差距至少相差近一倍。这个问题本质是由于多核竞争造成的,虽然每个虽然在例程一中每个goroutine都在操作不同的对象,但是这些对象处于同一个内存缓存行上,这就会造成本来没有并发竞争的程序,也产生了并发竞争问题。

MESI协议简介

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

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

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

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

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

但是在上面的例程一当中,四个goroutine操作的对象本质上处于同一个内存缓存行上,这也会造成S共享态到无效态迁移的频繁出现,从而影响效率。

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

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

我来说两句

0 条评论
登录 后参与评论

相关文章

  • Java、Go和Rust间的比较

    本文对Java、Go和Rust之间的对比并非完全是基准测试,更多的是比较输出的可执行文件大小、内存使用情况、CPU使用率、运行时要求,当然会有一个小基准测试用于...

    MikeLoveRust
  • 2018年伊始,系统编程语言Rust为何令程序员感到兴奋?

    自2013年年底以来,作者本人时断时续地会用Rust语言编程。4周前,再次用到Rust,语言比上次使用时更加容易(2016年5月)。这真的很令人兴奋!所以谈谈为...

    CSDN技术头条
  • Rust: 编译器驱动开发

    考虑到,我用这门语言的时间只有一星期多,某些观点和感受并非那么准确。因此,我的观点并不适合作为一份参考材料。

    Phodal
  • 【Rust日报】2019-10-03 rust-lang 主仓库突破 10,0000 次 commit

    截至发稿,rust-lang/rust 主仓库为 10,0006 次commit!!!

    MikeLoveRust
  • TIOBE:2016年全球1月编程语言排行榜 Java成2015年度语言

    Java的上涨齐头并进,Objective-C的跌幅(-5.88%)。苹果公司宣布Swift替换Objective-C的前一段时间是今年秋天的主要原因。据预计,...

    哲洛不闹
  • 编程语言2月最新排行榜出来了:最难的语言竟是它!

    经过 2 周投票,InfoQ 编程语言 2 月排行榜活动正式结束。基于用户的投票数据,我们不仅对程序员与编程语言的关系有了新的认识,而且还有一些有趣的发现。

    Java程序猿
  • 学会这五种编程语言,再来研究DevOps也不迟

    如何确保我们采用的DevOps能够成功?是否有某些语言非常适合应用于DevOps?今天,我们来看看众多编程语言中,哪个才是最适合DevOps的(顺序与排名无关)...

    企鹅号小编
  • 【深度知识】Rust语言入门、关键技术与实战经验

    编者按:高可用架构分享及传播在架构领域具有典型意义的文章,本文由唐刘在高可用架构群分享。转载请注明来自高可用架构公众号「 ArchNotes 」。

    辉哥
  • 【投稿】刀哥:Rust学习笔记 1

    近段时间在学习研究Rust。都说Rust学习曲线陡峭,感觉果然如此。之前学习Go,基本上没有专门去看语法,只是在需要的时候上网查一查,再花点时间看看大型的开源软...

    MikeLoveRust
  • 篇一 | 想全面了解 Rust 语言 ? 你想知道的都在这里

    本文为系列的第一篇文章,试图解答以下问题中的前五个,如果你感兴趣的问题不在其中,请回复评论。

    张汉东
  • 如何正确看待谷歌宣布Fuchsia操作系统没有选Go作为终端开发语言

    前段时间偶尔的机会下在 Go 圈被这条新闻刷屏了(当然还有 Go1.14 发布):

    IT大咖说
  • D语言架构师Andrei Alexandrescu谈D、Go、Rust取代C/C++

    【编者按】本文是D语言联合创始人、架构师Andrei Alexandrescu在问答Quora上关于“在取代C语言的道路上,D、Go和Rust谁的前途最光明?为...

    CSDN技术头条
  • ESR:程序语言设计的要诣和真谛

    为什么一些语言会成功,另一些语言会失败。 -- Eric Raymond 当你真正掌握了整体化的工程设计思维时,你就会发现高屋建瓴的工程设计已经远远超越了技术优...

    企鹅号小编
  • 2021 年,这8种编程语言最流行

    怎样判断哪种编程语言最流行?正如要挑选最受欢迎的冰激凌一样,每个人都有自己的最爱。实际上,由于各种原因,不同的开发人员喜欢不同的编程语言,当你认为一种编程语言可...

    庄闪闪
  • 现代编程语言哪家强?2020年,你或许应该了解这7门编程语言

    如果我们把人类的现代文明看作一辆汽车,那么软件开发行业就像汽车的发动机,编程语言就像发动机的燃料。那么问题来了,你应该学哪种编程语言?

    AI研习社
  • 一顿操作猛如虎,一看结果还是 0,Rust 能避免 Go 的 Bug?

    早些时候我看到这样一条新闻,在谈到Linux内核与Rust的关系时,谷歌曾表示Rust现在已经准备好加入C语言,成为实现内核的实用语言。它可以帮助减少特权代码中...

    MikeLoveRust
  • 【Rust日报】 2019-06-15:「实录」Rust 和 Go 在图像处理上的性能之争

    来自国内社区 NameFactory ,他在用Rust实现一门动态语言,并且在知乎里记录了他的心路历程。大家可以关注一下,支持一下。

    MikeLoveRust
  • 谷歌软件工程师:我为什么喜欢用Go语言?

    Go语言最近几年逐渐获得越来越多的开发者的喜欢。在Go社区前不久刚刚庆祝Go诞生10周年生日之际,谷歌云软件工程师Benjamin Congdon发表个人博客,...

    新智元
  • 坚持还是放弃,Go语言的“美好与丑陋”解读

    链接:https://bluxte.net/musings/2018/04/10/go-good-bad-ugly/

    23号杂货铺

扫码关注云+社区

领取腾讯云代金券