蜗窝科技

"内存屏障"浅谈[投稿]

蜗窝讨论区存档 · Linux kernel技术问答 · 楼主 karma303 · 2016-10-27 · 4 帖

本文是原「蜗窝讨论区」的历史存档(2016-10-27),来自版块「Linux kernel技术问答」,共 4 帖。讨论区已停止服务,此处仅供查阅。

karma303 · 2016-10-27 21:28

如果觉得可以的话, 希望能发到博客上. 不用给我开账号, 博主发上去就行了. 如果觉得太单薄了, 就仅作参考算了.

======================================== 因为要面试, 今天看了看"内存屏障"这部分. 网上这类文章很多, 但总感觉不得要领. 我尽量写短一些, 算是对wiki的补充. https://en.wikipedia.org/wiki/Memory_barrier 希望加起来能做为入门.

"内存屏障"是因为多核的出现才成为话题. wiki上举了一个应用的场合, 我把它完整的引用下来. 这个例子很好, 请一定不要跳过去.

** 写道:**

The following two-processor program gives an example of how such out-of-order execution can affect program behavior:

Initially, memory locations x and f both hold the value 0. The program running on processor #1 loops while the value of f is zero, then it prints the value of x. The program running on processor #2 stores the value 42 into x and then stores the value 1 into f. Pseudo-code for the two program fragments is shown below. The steps of the program correspond to individual processor instructions.

Processor #1:

while (f == 0); // Memory fence required here print x;

Processor #2:

x = 42; // Memory fence required here f = 1;

One might expect the print statement to always print the number "42"; however, if processor #2's store operations are executed out-of-order, it is possible for f to be updated before x, and the print statement might therefore print "0". Similarly, processor #1's load operations may be executed out-of-order and it is possible for x to be read before f is checked, and again the print statement might therefore print an unexpected value. For most programs neither of these situations are acceptable. A memory barrier can be inserted before processor #2's assignment to f to ensure that the new value of x is visible to other processors at or prior to the change in the value of f. Another can be inserted before processor #1's access to x to ensure the value of x is not read prior to seeing the change in the value of f.

"内存屏障"分为两种, 一种是compile-time的. 像上面core-2的代码, 完全可能被gcc优化成 f = 1; x = 42;

所以我们得加上一条"编译器级别"的"内存屏障": x = 42; asm volatile("":::"memory"); f = 1;

仅仅是一句"指导指令", 跟编译器打个招呼而已.

其实"内存屏障"这个名字取得不好, 我更愿意把它叫做"序列化指令", "分割线指令", 或者干脆叫做"分割线". 它上方和下方的代码被一分为二. 在编译优化的时候, 上面的代码不会被优化到"分割线"下方, 下方的代码也不会跑到"分割线"上面. 我这么说不严谨, 但我一直是这样记忆的. 很实用. 回到正文. 分割不是目的, 目的是有序. 这样, 就能保证, 编译出来的机器码, f=1; 在 x=42; 后面了.

一旦理解了编译器界别的barrier, cpu级别的其实如出一辙. gcc的优化,本来就相当于"编译器级别"的乱序. 像安腾, 就是完全把out-of-order交给编译器的. 接着wiki中的例子, 现在到了指令执行阶段: mov [x], 42 mov , 1 为了保证mov [x], 42 始终在 mov ,1 前面执行, 我们还是需要加个"分割线": mov [x], 42 cpuid mov , 1 如果你忘了什么是分割线, 我们再复述一遍: 分割线上方的不会跑到下面去, 下面的不会跑到上面去. 这是最表象的解释, 略微本质一点的讲, 就是cpu的乱序执行部件, 在执行完cpuid之前, 绝对不会执行cpuid后面的指令, 而且绝对会执行完cpuid之前的指令. 类似的指令还有iret, invd, lgdt等等, 不过都不能当"分割线"用, 因为要么是特权指令, 要么是side effect无法接受. 真正干净的还就只有cpuid一个.

这篇文章的基本部分就是这些. 只是讲了memory barrier最表象的功能,以及该怎么使用. 我猜很多人都知道, 但我觉得整理一下还是必要的, 网上缺少这种短小的入门文章.

[扩展部分] 下面延伸讲一些相关和不相关的东西. 我好像在哪儿看到过一种说法, 说"内存屏障就是把cache里的内容都写回到memory里". 当然不对. 我不愿意称呼"内存屏障", 甚至"memory barrier"这个词我都不愿意叫, 就是强迫自己摆脱这种思维. 我有段时间, 总以为"内存屏障"是关于memory,cache的. 但其实毫不相干, 它的作用是"序列化", 或曰"串行化". 也就是为了对付"乱序处理". 它使得我们对"乱序处理"有基本的控制, 哪两条指令不能乱, 我们就在中间插入一条这样的"分割线"指令. 其实像cpuid这种类型的指令, 在intel文档上就叫做Seriallizing Instructions. 它的定义是"These instructions force the processor to complete all modifications to flags, registers, and memory by previous instructions and to drain all buffered memory before the next instruct is fetched and executed". 大概是说, serializing instruction会把它前面的指令的作为都"冲刷". 就是说, 你修改了寄存器, 那我就让寄存器改变吧(因为pentium引入了寄存器重命名), 你写了内存, 那我把这些修改从cache冲刷到内存里吧. 你会问, 这跟"memory barrier"有什么关系? 是的, 一毛钱的关系都没有. 我上面说一大串, 纯粹是为了科普, Seriallizing instructions大概是干什么的. 再回到正文, Seriallizing instructions还有一个"小小的作用", 正是我们需要的, 它藏在在intel文档8.2.2<Memory Ordering>里的一大段经文一样的rules里: Reads or writes cannot be reordered with I/O instructions, locked instructions, or Seriallizing instructions.

就是说, 在执行到cpuid之前, 之前的指令会被执行完, 在执行完cpuid之后, 后面的指令才会开始运行. serializing instruction对"乱序执行"的冲击是很大的, 甚至指令的prefetch和speculatively execute都被它干掉了. 所以intel在pentium III引入了SFENCE指令, 又在pentium 4引入了MENCE和LFENCE. 并在文档里好言相劝, ...provide a more efficient method of controlling memory ordering than the CPUID instruction. 文档里强调了fence指令跟cpuid这种序列化指令的区别: "... are memory-ordering instructions, not serializing instructions. 1, These drain the data memory subsystem. 2, They do not serialize the instruction execution stream." 1,2两点其实就是serializes instruction主要的两个作用. fence指令只做到了第一点.(所以轻量些) 第一点我们刚才科普过, 再说句废话, 千万不要以为这个drain动作是冲刷整个cache. 不是, 只是冲刷之前指令所做的内存写.(被buffer在cache里), 用intel的话说, 就是ensures all pending memory transactions are completed. 第二点, fence指令不会像cpuid那样强烈的"序列化"指令流. 是有选择的序列化, 例如sfence只序列化指令流里的store类型的指令. 但lfence有些例外, 它确实保证了指令流的"序列化": LFENCE does provide some guarantees on instruction ordering. It does not execute until all prior instructions have completed locally. and no later instruction begins execution until LFENCE completes. 当然, 这就属于intel x86上的细节了. 我也是懒得追究, 所以读到了就赶紧记下, 但求别栽在上面就行了. 我写到这儿, 其实sfence, lfence在什么场合用, 以及它们的原理, 都还没有讲. 其实我也不很清楚,所以干脆一个字也不写好了. 下面是手册上的话. (sfence) serializes all store(write) operations that occurred prior to the SFENCE instruction in the program instruction stream, but does not affect laod operations. (lfence) serialize all load(read) operations that occurred prior to the LFENCE instruction in the program instruction stream, but does not affect store operations. 其实细究起来, 一旦分读写, 就复杂好多. 以后有时间再弄吧. 这已经算是一天的工作量了.

PS: 如果你有重要的回复, 请记得发信到jiejinmv@163.com告诉我一声, 我好来看看.

linuxer · 2016-10-28 19:16

写的挺好的,不过确实是条理性比较差,在博客上发表的文章我们还是希望能够条理化一些。

karma303 · 2016-10-29 12:36

linuxer 写道: 写的挺好的,不过确实是条理性比较差,在博客上发表的文章我们还是希望能够条理化一些。

没关系. 其实写的不好, 太单薄了. 我对"内存屏障"的本质不够了解, 只不过前天发现"fence"指令跟serializing instrution有某种亲缘关系, 就忍不住发出来了.

顺便问一下, 上一次wowo给我推荐的是不是华为呀. 我没去, 因为身子骨受不了. 不过还是谢谢 ... 怎么今天才想到说谢谢.

wowo · 2016-10-31 10:07

karma303 写道:

linuxer 写道: 写的挺好的,不过确实是条理性比较差,在博客上发表的文章我们还是希望能够条理化一些。

没关系. 其实写的不好, 太单薄了. 我对"内存屏障"的本质不够了解, 只不过前天发现"fence"指令跟serializing instrution有某种亲缘关系, 就忍不住发出来了.

顺便问一下, 上一次wowo给我推荐的是不是华为呀. 我没去, 因为身子骨受不了. 不过还是谢谢 ... 怎么今天才想到说谢谢.

哈哈,不用客气。 话说网站上有很多公司HR、猎头等,其实不需要我推荐,他们可能会自动找你