一个系列:之一

外面的世界总是充满了危险性,因此在开始探险之前应该做好充分的准备。即使有些准备怎么看来也是多余的,但在关键时刻却能发挥很大的用途. 我现在即将去冒险,这途中可能会遇到很多有趣的事情,收获,也会接到。不过这都无所谓,是我为成功的准备(匹克运动鞋广告?!)。
控制论中,线性行为是最简单的。而我发现世界喜欢简单的东西于是有了我喜欢线性系统的说法。事情在CPU的世界中本来也是这样进行的。可是有一天,我们发现线性是“低效”的(请注意这个引号)。为什么呢?因为指令总是必须搭乘固定时刻的车走,如果恰巧车来的时候排在前面的人不上,则会浪费一到几趟车的时间(当然,这个比喻并不恰当),于是我想,我是不是可以加塞儿?虽然这可能得到众人的强烈鄙视,但是却可以最大限度的利用这有限的资源。于是线性的世界被打破了。队列中出现了不可预测的加塞儿现象,称为“非顺序执行(Out-of-order execution)”。众多的学者在上世纪70到80年代在这个问题上进行了众多的研究。大家都知道,研究人员喜欢作定义以便交流,于是我们也做个定义。

非顺序执行:英文称作 Out-of-order execution,也称乱序执行。是一种在高性能CPU设计中所广泛采用的技术。这个技术企图最大限度的利用CPU的各个周期,改变指令的执行顺序,以降低慢操作或其他操作造成的延迟。这里所说的改变执行顺序,是CPU的行为,不是编译器的行为!

支持非顺序执行的处理器出现的很早(1960)但是我们耳熟能详的却是在1995年出现的一代“划时代的与垃圾的”处理器(Intel Pentium Pro),与AMD K5处理器(1996)。此后,乱序执行被一代代高性能处理器视为必须拥有的功能而流传至今(如果你对乱序执行的算法感兴趣,可以看看较早的乱序算法--Tomasulo算法。)
与现实世界中让人厌烦的加塞儿不同,处理器中的乱序执行让人看起来好像就像顺序执行一样,逻辑并没有发生丝毫改变。于是我们为这个伟大的发现欢欣鼓舞了很长时间。但是有一天,聪明的程序员试图用多线程(可能一开始是为了解决用户界面阻塞的问题)时却发生了令人困扰的错误。结果,困扰的原因是我们第一句话漏了三个字:单线程。实际上,CPU的乱序执行能力保证在单线程环境下改变执行的逻辑。例如以下的代码片段(我们的默认语言是C#因为这是一个.NET博客):

(我们认为,测试环境是在 IA-32 平台或者 IA-64 平台之下,在.NET环境下)

1 int x = 1, y = 2;
2 volatile int a = 1;
3 // 中间可能有其他代码
4 = 2// st.rel
5 while (y == 1); // ld
6 = y; // ld.acq

问题是,以上的代码果真会按如程序所示的顺序执行么?实际上可能是以下的顺序。

1 = 2// st.rel
2 = y; // ld.acq
3 while (y == 1); // ld


但是从单线程的逻辑上来看,两种执行顺序完成后都是同样的一个效果。但是在多线程环境下,由于执行顺序的改变就可能造成问题了!!
慢!可能现在已经要晕了。你最关心的可能并不是后果是什么,而是关心这种顺序的改变是谁造成的,什么时候造成的。我们首先明确一下顺序的概念。顺序有三种:

(1)程序顺序:指在特定CPU上运行的,执行内存操作的代码的顺序
(2)执行顺序:指在CPU上执行的独立的内存相关的代码执行的顺序。执行顺序和程序顺序可能不同,这种不同是编译器和CPU优化造成的结果。我们上述所谓的执行乱序就是指执行顺序中,由CPU带来的执行顺序的改变。
(3)感知顺序:指特定的CPU感知到他自身的或者其他CPU对内存进行操作的顺序。感知顺序和执行顺序可能还不一样。这是由于缓存优化或者内存优化系统造成的。

用白话说就是这个样子的:

话说你某一天编写了一段代码-->你在编译器上进行编译发现很顺利(但是在编译的过程中可能你的代码顺序已经被改变了,这种改变应该属于执行顺序改变)-->你试图运行目标代码(目标文件中的代码的顺序应该是程序顺序)-->你的代码开始被执行(但是执行过程中CPU对内存操作进行了乱序,这属于执行顺序的改变,也是我们上述例子不断在说的事情)-->你的部分代码执行完毕之后各个CPU对执行结果的感知的顺序可能也是不同的(例如,CPU 1操作了内存单元1进而操作了内存单元2,但是另一个CPU先看到了内存单元2的改变而后又看到了内存单元1的改变)。

结论:我们探讨的问题是“执行顺序”中由CPU改变的那一部分。

好的,在我们继续讨论之前我们可以探讨一下,我们的Daily Work中有哪些行为可能会对上述的各种顺序造成影响呢?列个不完全清单(注意!我们讨论的模型还是在.NET内存模型下):

(1)当你使用 volatile 的时候的时候,你阻止了编译器对相应内存代码的优化。阻止了大部分的编译器造成的执行顺序的改变。但是你根本不能够阻止CPU对执行顺序的改变(除非处理器有意如此,例如IA-64处理器会在volatile访问时安插Memory Fence)!因此Intel技术博客上有文章指出:“volatile 对于多线程编程几乎没有任何用处”。
(2)当你使用Memory Barrier或者Memory Fence的时候你从某种程度上(取决于你使用了哪一种)阻止了CPU和编译器对执行顺序的改变。

好了,这就是第一篇。目的是说明:在上路之前还有好多准备工作要做。下一篇就具体针对.NET环境说说如何应对Out of order execution。

posted @ 2008-06-15 13:36  TW-刘夏  阅读(1816)  评论(3编辑  收藏