第93章 安腾指令

就市场来讲,安腾(Itanium)处理器几乎是失败产品。但是它的Intel Itanium(IA64)架构非常值得研究。乱序执行(OOE)CPU理念,侧重于让CPU重新划分指令的片段和顺序,再把重组后的指令组分派到并联计算单位进行并行计算。而英特尔(Intel)安腾架构推出的并行计算技术(Explicitly Parallel Instruction Code,EPIC),则主张让编译器在编译的早期阶段实现指令分组。

厂商推出了配合这种并行计算技术的编译器。不过,这些编译器因异常复杂而颇受争议。

本章从Linux内核(3.2.0.4)摘录了部分IA64指令。这段程序用于实现某加密机制。其源代码如下所示。

指令清单93.1 Linux kernel 3.2.0.4

#define TEA_ROUNDS                32
#define TEA_DELTA                 0x9e3779b9

static void tea_encrypt(struct crypto_tfm *tfm, u8 *dst, const u8 *src)
{
          u32 y, z, n, sum = 0;
          u32 k0, k1, k2, k3;
          structtea_ctx *ctx = crypto_tfm_ctx(tfm);
          const __le32 *in = (const __le32 *)src;
          __le32 *out = (__le32 *)dst;

          y = le32_to_cpu(in[0]);
          z = le32_to_cpu(in[1]);

          k0 = ctx->KEY[0];
          k1 = ctx->KEY[1];
          k2 = ctx->KEY[2];
          k3 = ctx->KEY[3];

          n = TEA_ROUNDS;

          while (n-- > 0) {
                   sum += TEA_DELTA;
                   y += ((z << 4) + k0) ^ (z + sum) ^ ((z >> 5) + k1);
                   z += ((y << 4) + k2) ^ (y + sum) ^ ((y >> 5) + k3);
          }

          out[0] = cpu_to_le32(y);
          out[1] = cpu_to_le32(z);
}

其编译结果如下所示。

指令清单93.2 Linux Kernel 3.2.0.4 for Itanium 2(McKinley)

0090|                                  tea_encrypt:
0090|08 80 80 41 00 21                     adds r16 = 96, r32     // ptr to ctx->KEY[2]
0096|80 C0 82 00 42 00                     adds r8 = 88, r32      // ptr to ctx->KEY[0]
009C|00 00 04 00                           nop.i 0
00A0|09 18 70 41 00 21                     adds r3 = 92, r32      // ptr to ctx->KEY[1]
00A6|F0 20 88 20 28 00                     ld4 r15 = [r34], 4     // load z
00AC|44 06 01 84                           adds r32 = 100, r32;;  // ptr to ctx->KEY[3]
00B0|08 98 00 20 10 10                     ld4 r19 = [r16]        // r19=k2
00B6|00 01 00 00 42 40                     mov r16=r0             // r0一直是0
00BC|00 08 CA 00                           mov.i r2 = ar.lc       // 保存lc
00C0|05 70 00 44 10 10 9E FF FF FF 7F 20   ld4 r14 = [r34]        // load y
00CC|92 F3 CE 6B                           movl r17 = 0xFFFFFFFF9E3779B9;; // TEA_DELTA
00D0|08 00 00 00 01 00                     nop.m 0
00D6|50 01 20 20 20 00                     ld4 r21 = [r8]         // r21=k0
00DC|F0 09 2A 00                           mov.i ar.lc = 31       //TEA_ROUNDS=32
00E0|0A A0 00 06 10 10                     ld4 r20 = [r3];;       // r20=k1
00E6|20 01 80 20 20 00                     ld4 r18 = [r32]        // r18=k3
00EC|00 00 04 00                           nop.i 0
00F0|
00F0|                                  loc_F0:
00F0|09 80 40 22 00 20                     add r16 = r16, r17  //r16=sum, r17=TEA_DELTA
00F6|D0 71 54 26 40 80                     shladd r29 = r14, 4, r21 // r14=y, r21=k0
00FC|A3 70 68 52                           extr.u r28 = r14, 5, 27;;
0100|03 F0 40 1C 00 20                     add r30 = r16, r14
0106|B0 E1 50 00 40 40                     add r27 = r28, r20;;     // r20=k1
010C|D3 F1 3C 80                           xor r26 = r29, r30;;
0110|0B C8 6C 34 0F 20                     xor r25 = r27, r26;;
0116|F0 78 64 00 40 00                     add r15 = r15, r25       // r15=z
011C|00 00 04 00                           nop.i 0;;
0120|00 00 00 00 01 00                     nop.m 0
0126|80 51 3C 34 29 60                     extr.u r24 = r15, 5, 27
012C|F1 98 4C 80                           shladd r11 = r15, 4, r19 // r19=k2
0130|0B B8 3C 20 00 20                     add r23 = r15, r16;;
0136|A0 C0 48 00 40 00                     add r10 = r24, r18       // r18=k3
013C|00 00 04 00                           nop.i 0;;
0140|0B 48 28 16 0F 20                     xor r9 = r10, r11;;
0146|60 B9 24 1E 40 00                     xor r22 = r23, r9
014C|00 00 04 00                           nop.i 0;;
0150|11 00 00 00 01 00                     nop.m 0
0156|E0 70 58 00 40 A0                     add r14 = r14, r22
015C|A0 FF FF 48                           br.cloop.sptk.few loc_F0;;
0160|09 20 3C 42 90 15                     st4 [r33] = r15, 4    // store z
0166|00 00 00 02 00 00                     nop.m 0                 
016C|20 08 AA 00                           mov.i ar.lc = r2;;    // restore lc legister
0170|11 00 38 42 90 11                     st4 [r33] = r14       // store y
0176|00 00 00 02 00 80                     nop.i 0
017C|08 00 84 00                           br.ret.sptk.many b0;;

上述IA64指令很有特点。

首先,每3条指令构成一个指令字(instruction bundles)。每个指令字的长度都是16字节即128位,由1个5位的模版字段和3个41位微操作指令构成。IDA把这些指令组分为(6 + 6 + 4)字节的结构体,以便于调试人员进行区分。

除了含有停止位(stop bit)的指令之外,这些由3条微操作指令构成的指令字,通常都由CPU并行处理。

据称,Intel和HP的开发人员针对常见指令进行了模式划分,从而推出了指令字类型(即指令模版)的概念——声明指令字运算资源的模版字段。CPU依此把指令字区分为12种基本类型(basic bundle types),基本类型又分为带停止位的版本和不带停止位的版本。举例来说,第0类指令字叫作MII类指令字,依次由内存读写微操作指令(M)和两条整数运算的微操作指令(II)构成;最后一类指令,即0x1d类指令字,又叫作MFB类指令字,依次由内存读写微操作指令(M)、浮点数运算微操作指令(F)、分支(转移)微操作指令(B)构成。

如果编译器在指令字的指令位(instruction slot)上编排不了相应的微操作指令,那么它可能在这些指令位上安插空操作指令nop。您可能注意到了,本文中的nop指令分为“nop,i”和“nop,m”。i代表该nop指令占用整数(integer)处理单元,属于整数运算型微操作指令;m代表它占用内存处理单元,属于内存操作型微操作指令。在人工编写汇编语言时,编辑程序会自动插入相应的nop指令。

IA64汇编指令的特性不止这些。该平台的指令字还可进行分组,构成指令组(instruction group)。指令组可由任意个连续运算的指令字和一个含有停止位的指令字构成,是一个可并行执行的指令集合。在实际应用中,安腾2处理器可以并行执行2路指令字,即同时处理6个微操作指令。

这就要求指令字中的各微操作指令和每个指令组的各指令字之间互不干扰,即不存在数据竞争。如果存在数据竞争、形成脏数据,那么运算结果不可控(undefined)。

在IDA中,微操作指令之后的两个分号(;;)表示该指令有停止位。可见,[90-ac]及[b0-bc]分别属于可并行执行的两个指令组,它们之间不存在互扰。下一组则是[b0-cc]。

另外,在10c处的指令,以及下一条位于110处的指令都有停止位。这就意味着CPU会在与其他指令隔绝的情况下运行这两个指令,这种运行模式就和常规的CISC/复杂指令集的执行方式完全相同了。这是由于后续指令,即110处的指令需要前一条指令的运行结果(R26)寄存器,所以不能并行处理这两条指令。很明显,此时编译器不能找到更好的并行处理手段,无法更有效地利用CPU,所以在此添加了2个停止位和多个NOP指令。虽说编译器在智能方面很不成熟,但是人工的IA64汇编编程也丝毫不轻松:程序员要手动完成指令字分组的工作。

要图省事的话,程序员可以给每条指令添加停止位,不过这将大幅度地浪费安腾处理器的运算性能。

Linux内核的源代码中,就有一些经典的、手写IA64汇编代码。有兴趣的读者可参考:http://lxr. free-electrons.com/source/arch/ia64/lib/

有关IA64汇编语言人工编程的具体方法,可参见Mike Burrell撰写的专业论著《Writing Effcient Itanium 2 Assembly Code》(http://phrack.org/issues/57/5.html