【转贴】多核环境下的内存屏障指令

呵呵,工作小息 ,抽空看了云风的BLOG,发现又有好东西值得学习了,由于多核的编程之前接触得比较少,所以有了这样的文章,还是很感动的。当然不能忘记感谢前辈们百忙之中抽空写文章,下面是转贴的内容

 

本来不打算立刻写关于这次 软件开发大会 的事情。太多可以写的东西,反而不知道怎么写起。今天才有机会上网到处转转,转到 周伟民老师 的 blog 上,看到这么一篇 。里面既然提到我,就想在上面回上两句。可惜 csdn 的 blog 系统实在是太烂了(这个话题我们在周六的沙龙上集体声讨过,暂且按下不表),硬是没发上留言。那么我还是在自己的地盘单独提出来说说吧。


周老师那个 session 正好排在我的前面。同一间会议室,而且内容我也颇有兴趣。也就顺理成章的听了。讲的东西其实满不错的,唯一的抱怨是太像在大学里授课,互动少了点。会场气氛远不如后来 Andrei 讲 Lock-Free Data Structures 那么精彩。

周老师讲的这块内容,正巧我前几年写多线程安全的内存分配器时碰到过,有点研究。加上前几年对 Intel 的东西颇有兴趣,便有了发言的冲动 :) 。当时的会场上下文环境正好是有个朋友提问说:实际上,InterlockedIncrement 的调用是多余的。(事后交换名片得知,提问的这个哥们是来至 google 的程序员)

如果换在几年前,我是赞同这个哥们的观点的。记得 04 年左右,我们公司内部的 maillist 上曾经有类似的讨论。即,在 32 位系统上,写一个 dword 本身就是原子的,如果 cpu 可以保证程序逻辑上的执行次序(program ordering),那么简单的利用写操作就可以替代锁操作。我们在操作完一大片内存数据后,只需要在最后更改关键的标记字,那么不需要加锁也可以保证安全。(还有一个隐含的前提是数据必须被 32bit 对齐)

btw, 当天晚上 Andrei 讲 Lock-Free Data Structures 时向大家提的问题:那个 hazard list 为什么要用单向链表实现?大约也是这个意思,因为链表指针可以被原子的修改而无需加锁。

单核时代它是对的,因为单核 CPU 要求读写操作 self-consistent 。多嘴两句解释一下,现代 CPU 工作时的指令执行次序(process ordering)是可以不同于程序编制的次序(program ordering)的,即乱序执行技术。这个技术可以极大的提高流水线的工作效率。单核 CPU 保证读写操作的 self-consistent 意味着,等到真正读入操作数据时,数据符合 program ordering 上的正确性。

可问题也出在这里。随着多核的发展,为了提高每个核上的流水线效率,多核环境不再保证其安全。在每一个核上,cpu 内部工作时指令都可能被乱序执行。那么逻辑次序上后写入内存的数据未必真的最后写入。核与核之间作为一个整体看的话,却不保证 self-consistent 了。

也就是说,如果你不做多余的防护措施,在一个核上写入一批数据后,如果你期望最后写一个标记数据表示前面的数据都已经准备好;然后从另一个核上依靠判断这个标记位来判定一切数据就绪。这个策略并不可靠。标记位可能被先写入,但其它数据尚未真正写入内存。

解决的方法是,在标记位被写入前,强迫 CPU 串行化。InterlockedIncrement 和它的兄弟们可以提供这种安全性。翻译成 Intel 指令,会发现它在汇编指令前加了 lock 前缀。也就是在这些读写内存的指令在发起时,cpu 会向总线发出一个 lock# 的信号,阻塞住其它内存访问请求。

但这么做未免效率太低。这种影响总线的指令会随着核越来越多而变的越来越低效。可以想象,任意一个核上发起 lock 几乎会让所有的核都短暂的停止工作(除非完全不访问内存?)。我们今天只有两个或四个核,性能影响微乎其微。但是等到机器拥有 32,64 甚至更多核时,就可能相当严重了。

ps. 上面这个说法也不全然正确。因为既然内存锁在多线程编程中运用的非常广泛,自然在芯片设计上是要做优化的。在 Pentium Pro 以后,当被访问内存处于 cache 中时,lock# 信号不会被发到总线上,取而代之的是锁住 cache 。这样代价会小的多,但是在某些情况下依旧昂贵。(当多个核 cache 住同一块内存时会受影响)

轻量一点的方案是执行一条 CPUID 指令,它也可以保证前面的操作被串行化。到了Pentium III ,Intel 在 IA32 指令集中增加了 SFENCE 指令用来提供更细的控制粒度以更少的代价解决这个问题。在指令序列中插入 SFENCE 可以保证在此指令之前的写操作全部完成(非写操作的指令依旧允许乱序执行)。这样我们在另一个核里读相同内存时,几乎不会出错。

在这里我用了“几乎”,是因为诸如访问单项链表,判断标志数据的编程逻辑,对内存的读操作都是上下文相关的。我们可以断言执行次序不会被乱序执行影响。构造一个可能因为乱序读内存而有出错隐患的合适例子不太容易。但是从 Pentium 4 开始,严格上讲,我们需要用 LFENCE 指令(Pentium 3 没有提供也不需要这条指令)配合使用。它可以保证在此之前的 program ordering 上的读内存操作已经完成(否则逻辑结果可能因为多核间同步 cache 等原因而受到影响)。另外有 MFENCE 指令粒度粗一点,可以同时保证读写内存操作都已完成。


本来不打算翻资料,凭印象写的。写完这篇 blog 审了一遍,还是担心留下重大错误。就又一次翻阅了 2005 年在 Intel 网站上免费索取的 IA-32 Intel Architecture Software Developer's Manual Volume 3 的 Chapter 7 :Multiple-processor management 。

核对后,我想大概意思应该写清楚了,如果有小 bug 还请行家多多包涵。真有兴趣把这点东西搞清楚的朋友,莫信我的一家之言,查 Intel 的手册吧,它写的更加清楚和权威。btw ,不要问我该去哪找,去问 google 。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值