如果想得到IA-64体系架构软件开发手册的中文版,购买周明德教授“编写”的《64位处理器应用级编程》一书是一个捷径。该书全部内容直截了当的翻译了INTEL和AMD的64位体系架构软件开发手册的应用卷,通篇没有一句是作者自己的话。不过,毕竟是翻译,对于想从64位入手而对英文不熟悉的编程爱好者们来说,直接看周教授的书也是一个不错的选择,该书唯一差的地方是,没有把应用卷给翻译全,而且只翻译了应用卷,没有系统卷和指令集这两部分,不然别说书的价格是二十几块钱,二百块我也想买一本。

在此,作为一名计算机编程爱好者,本人愿意尽自己能力,对该书的不全之处做出力所能及的补充。本篇打算补充的内容,位于手册的应用卷第三章第3节至第三章结束。是有关指令编码的概要以及指令的执行方式的。手册的内容懒得直接翻译了,干脆以我自己的话来讲我想可能来得更顺溜。

IA-64体系架构的一共153条指令是被归类了的,一共有六大类:ALU整型(A型)、非ALU整型(I型)、存储器访问(M型)、浮点操作(F型)、转移操作(B型)以及扩展双槽指令(L+X型)。特定类型的指令由特定类型的指令执行单元来负责执行,IA-64提供了四种执行单元类型:I型、B型、M型、F型。其中,A型指令可以由I型或M型执行单元来执行,扩展双槽指令由B型或I型执行单元来负责,其余的指令类型和执行单元类型一一对应。

IA-64引入了指令包的概念,一个指令包包了三条指令。IA-32的指令在存储器中是一条接一条的排列,而IA-64是一个指令包一个指令包的排列。指令包长128位,在存储器中必须按128位对齐的方式,从低地址到高地址顺序排列。下面是从手册上摘抄下来的指令包格式的图示:


可以看出,一个指令包的0-4位为模板位,模板位给出了包中指令的执行方式。IA-64体系架构一条指令有41位,在包中,位5-45、46-86、87-127分别用于容纳三条指令的位置。IA-64的指令序列就是一个指令包的指令槽0、指令槽1、指令槽2,然后到下一指令包中的指令槽0、指令槽1、指令槽2等等这样一直下去。但是由于指令模板,包中的指令可能会由不同的指令执行单元来执行,因此,可能会并行执行,这是IA-64与IA-32的一个很大的不同点。

下图给出指令模板的格式图的一个部分:


举例来说,如果模板位的值为00(表格的第一项),那么就是告诉硬件,指令槽0所包含的指令,由M型指令执行单元来执行,指令槽1和2所包含的指令由I型执行单元负责。

IA-64体系架构的指令包不但包含了指令以及指令执行方式的信息,还包含了指令执行时的暂停信号。在指令模板格式图示中,用双线来表示暂停,例如,模板位为01的指令包,在指令槽2后面有一个暂停信号,IA-64的暂停信号是告诉硬件,暂停信号前后两端的指令会有资源依赖,也就是说信号前的指令读/写的资源可能信号后的指令也会读/写,因此不能将两端的指令并行执行,在此用暂停信号强制执行两端指令的先后顺序关系。

在此做一个插曲,IA-64与IA-32有一个决定性的不同在于,IA-64是指令级并行架构,也就是说,由于不同的指令执行单元可以同时工作,只要将不同类型的指令指派到不同的执行单元,那么指令就可以是并行执行的,并行执行在IA-32中虽然也有,但是32位架构当中是由硬件来负责并行执行的,在32位软件眼里,硬件还是一条一条执行指令的,但是到了IA-64架构下,由指令包的格式可以看出,原来的32位环境下的硬件的后台并行机制现在被带到了前台,由软件来负责了,因此汇编器和汇编程序员们必须要亲自负责指令的并行性挖掘工作。

IA-64体系架构的指令的执行一共有四步:1. 从存储器中读取指令(取指令);2. 若必要则读取体系架构的状态信息(读状态信息),这里的体系架构的状态信息指的是某个寄存器的值或某个存储器地址的值;3. 完成指令指定的操作(执行指令);4. 若必要则更新相应的体系架构状态信息。例如:指令add r1 = r2, r3就是将通用寄存器r2和r3的值相加,结果放入寄存器r1中。指令执行的第一步是先从存储器中取出该指令,然后,读取架构状态信息,该指令的加和运算需要寄存器r2和r3的值,因此这里的架构状态就是指r2和r3的值,第三步是将两个值相加,第四步,更新寄存器r1,更新的值为前面相加的和。指令执行的第2步和第4步不是必需的,例如如果一条指令将一个立即数写入一个寄存器,那么第2步就不必要了。

IA-64的汇编代码由指令序列和指令序列中的暂停信号组成,上一个暂停至下一个暂停中间的指令序列就构成了一个指令组(不严格的说法),严格的说,指令组是从一个给定的指令槽号开始,顺序直到第一个暂停信号、转移指令、中断指令或非法操作错误为止的指令序列,由于暂停信号代表了信号两端的指令存在资源依赖,那么就可以先简单的认为,指令组中的指令没有资源依赖,也就是可以并行执行。

IA-64指令的执行有几个规则:

(1)               一条指令的取指令阶段与任何动态的前先指令的读状态、更新状态、执行等没有因果关系。因此我猜想,可能IA-64的任何指令都要经历一个取指令的阶段,哪怕前面的指令执行导致该指令不会执行,但该指令还是会被取出。

(2)               在指令组与指令组中,一个给定指令组中的每条指令的读取架构状态的阶段,要等到前一个指令组的所有指令更新完架构状态才开始进行。由于指令执行的结果从本质上看就是更新了架构状态信息(例如转移操作的本质就是重写了IP寄存器),那么如果把IA-64一个指令组做为一个整体看成一个大指令,那么这个大指令的执行方式可以看成是非流水线化的。

(3)               在同一个指令组内,每条指令在读取架构状态信息时,如果读取的是存储器,那么这个指令的读取存储器的阶段会等到所有先前的指令完成对存储器的更新之后再进行。

以上三个规则都是符合正常思维的,也就是说这些规则几乎等同于一条指令先取然后再读架构状态,执行操作再更新架构状态,然后接着处理下一条指令。要命的是下一个规则:

(4)               在一个指令组中,每条读取寄存器状态信息的指令,注意这里是寄存器,它的读取状态信息的阶段都是先执行的,不管前面的指令有没有对寄存器状态进行更新。

举个例子,假设寄存器r1、r2、r3、r4值分别为1、2、3、4。现在我要将r2和r3的值相加后的值先置入r1,然后将r1的值拷贝到r4,程序该这么写:

add r1 = r2, r3

move r4 = r1

如果这两个指令在同一个指令组中,程序会出现问题,r4的值可能会是1(没有做过上机试验,因为本人没有IA-64机器),而不是r2加上r3的结果5。为什么呢,因为根据第4条规定,所有读取寄存器状态信息的阶段都是先执行的,所以move指令会在前一条add指令未更新r1寄存器的时候,就先读取r1的值,这样读取的值是r1原来的1,而不是加过以后的5。所以解决的办法是写成:

add r1 = r2, r3

;;

move r4 = r1

IA-64的汇编器默认使用两个分号来代表暂停,这里的意思就是将两条指令分到两个不同的指令组中去。

象以上这种前面的指令先写了r1,后面的指令要读取先面写过的r1的值的情况,叫做先写后读依赖,一个指令组中不允许寄存器有这样的依赖,因为会出现逻辑错误。办法就是将两条指令分到两个指令组中去。一个指令组中禁止的不仅有寄存器的先写后读依赖,还有先写后写依赖,就是前一条指令写了某个寄存器,后面有一条指令又写了同一个寄存器,如果出现这种情况,这两条指令也必须被分隔到不同的指令组,这种规定的原因是什么本人目前还不太清楚,猜想大概是因为编译器在做优化的时候可能会对指令组中的指令进行重新排序,如果这两条指令出现在同一指令组中,顺序又被颠倒的话,就会导致程序的逻辑错误吧。

指令组中禁止了寄存器的先写后读依赖和先写后写依赖,但是对存储器的访问没有限制,也就是说,具有存储器资源依赖的指令可以位于同一指令组中,由于需要访问存储器状态的指令的读状态和更新状态的次序是按照指令的次序规规矩矩来的,只要指令的顺序正确,得到的会是预期的结果。

IA-64有专门的机制用于对付对访问存储器的指令进行乱序处理的情况,也就是所谓的猜测执行机制。猜测执行机制与指令包的并行执行都属于程序优化技术的一个部分,在IA-32下都是由硬件来负责在后台默默的完成的,在IA-64下,指令打包与存储器的猜测访问指令使得这两项工作开始变成由软件负责解决了,指令打包和存储器的猜测访问是IA-64对于IA-32的一个很大的不同点。