典型的 X86 程序的代码中大约有50%的指令是存储器访问指令,其中存储器读指令大约是存储器写指令的2倍。然后,大约15%到20%的指令是分支指令(if, then, else等)。剩余指令中,大部分是诸如“ADD”、“MUL”这样的较简单的计算指令。像“DIV”、“SQRT”这些较复杂的计算指令在所有指令中只占很少的一部分。所有这些指令都按照典型的流水线步骤执行:取指,解码,取操作数,执行,退出。
首先,处理器会根据指令指针寄存器(instruction pointer register)指示的地址取回指令。这时,对处理器来说,指令仅仅是一些没有意义的0、1字符串。只有在被解码之后,指令对处理器来说才开始有意义。指令被解码后可以得到操作数地址和操作码,而操作数地址可以在下一步发挥作用:取操作数。你不会希望处理器对操作数的地址进行计算,而是对那些地址里面存放的内容进行计算――与 C 语言里面的指针的概念很相似。当操作数被取出来以后,ALU根据操作码的指示,就可以对操作数进行正确的计算了。计算结果一般将被写回处理器内部的寄存器堆中。有时候,计算结果也需要写回到缓存和内存中。这就是最后的步骤――退出。到此为止,你应该略微了解一条指令的整个执行过程了。
这很容易理解。假设现在有一个 ALU 操作需要某数据,可是该数据不在一级缓存中。如果 load 该数据的操作在该 ALU 操作之前就已经执行完毕,那么访问二级缓存的延迟就不会对性能产生影响。不过,需要注意的是,如果 load 操作针对的数据在程序中还有 store 操作要对其进行写入,那么就不能把 load 操作提前到该 store 操作之前执行。因为这样的情况下,如果提前执行 load 操作的话,意味着你得到的会是错误的数据,而不是最新的。
Intel 内存相关性预测技术
上图中的 Load 2 操作不能提前执行,因为它操作的数据与 Store 1 操作的数据相同,需要等待 Store 1 操作先完成。只有 Store 1 执行完毕,数据Y才拥有正确的值。不过 Load 4 操作没有理由不能提前进行,它不需要等待 Store 1 或者 Store 3 操作完成。这样,通过把 Load 4 操作提前,load 单元有更多的时间去获得正确的操作数。
不过,之前的处理器在这种情况下――有 store 操作存在――都不会把 load 操作提前。因为处理器不知道 store 操作针对的数据单元与 load 操作是否相同。如果想要搞清楚是否相同的话,需要计算存储器地址。这十分困难,因为在指令乱序和调度的时候,存储器地址还是未知的。
再来看一下采用 Core 微架构的服务器产品Woodcrest,考虑到服务器应用程序中能够发挥指令级并行能力的地方并不多,Core 微架构没有应用超线程技术应该是它仅有的缺点。这个小缺点是 Core 微架构的设计思想导致的,因为 Core 微架构需要顾及服务器平台、桌面平台和移动平台。这也许会使 Sun 公司和 IBM 公司在某些需要进行多线程应用的服务器平台上得到机会。不过,采用 Core 微架构的4核产品 Tigerton 很快就要到来,也许可以弥补这个劣势。那么,现在我们的读者应该清楚了:你很难从 Core 微架构中找出明显的缺陷。
不过,具有讽刺意义的是,就在一年前,Intel 还并不重视提升IPC(Instructions Per Clock)和ILP(指令级并行能力)。多核被认为是未来的发展趋势,而单核的性能似乎无关紧要。因此在 Dobbs 博士的文章中曾经提到“The free lunch is over”(免费的午餐结束了),他认为以后只有增大缓存才能带来IPC的微弱提升,开发者把注意力集中在处理器的IPC效率上的日子已经一去不复返了。一些研究者甚至认为,具有简单的、顺序执行的架构的处理器才是未来的方向。
我们却非常怀疑“Threading is our only savior”(多线程是唯一的救世主)。Unreal 3 游戏引擎的开发者 Tim Sweeney 曾经指出,在下一代游戏引擎中开发多线程的代码是非常大的挑战。宽流水线、高频率的处理器被否定得有一些过快。NetBurst 微架构的设计使用了 LVS 电路设计的策略来实现极高的频率,而 Core 微架构并没有采用这种策略。但是,它仍然是一个采取极宽流水线的、采取乱序执行策略的处理器,这是那些不想花费太大力气在编写多线程应用程序上的程序员的免费的午餐。从这个角度来说,对双核处理器发展的需求可能不会那么太迫切。让用户得到更高的性能是软件开发者和处理器设计者共同的责任。是的,双核是好东西,但是单核的性能仍然重要。