这是CSAPP第四章,作者想通过自己定义的Y86-64的ISA指令集定义、设计、编码以及制造一个简单的单周期流水线化的处理器。作者的思路也非常清晰,包括了上述的所有步骤,在每一步描述了嵌入了处理器设计的基本原则以及需要注意的技术细节。下面是设计的ISA的指令
前置知识
堆栈操作
作者在4.1.6和4.3.3两次提到堆栈操作的特殊性,此处做一个基本的回顾。堆栈指令包括 popq
和 pushq
两条,作者设计假定栈顶的地址始终保存在寄存器 %rsp
中,参考程序的机器级表示中的内容,有如下示意图
在理解上面过程之前,我们假设:
- 堆栈中保存的是 8字节的元素,而且按照小端法存储的,即低有效位对应低地址;
- 数据在内存中的地址,指的是整个数据的第一个字节在内存中的地址,例如保存一个2字节的数据
0x1234
在内存地址0x100处,那么0x100地址保存字节0x34,0x101处保存字节0x12。
结合上面的假设,很容易看出,
- 堆栈是从高地址向低地址增长的,推入一个元素栈顶指针地址变小,弹出一个元素则变大;
%rsp
始终指向栈顶的最后一个元素的地址,也就是最后一个元素在内存中的起始地址。
基于上面的两点,程序的操作就比较好理解了,初始状态 %rsp
中地址是0x108,
- 将
%rax
寄存器中的数据push进去,所以堆栈元素要增加8字节,为了将新的元素放到堆栈里面,先执行%rsp = %rsp - 8
,此时%rsp
指向要入栈的新元素的起始地址0x100,这时就可以根据%rsp
的起始地址连续存储8个字节的新元素了,这个操作类似于1 2
subq $8, %rsp // rsp = rsp - 8 movq %rbp, (%rsp) // %rbp的元素拷贝到%rsp存储的地址处
- 出栈就是相反的,当前的
%rsp
保存的就是需要弹出元素的地址,那么可以直接将该地址开始的连续8个字节先拷贝到目的地址,然后%rsp = %rsp + 8
,此时%rsp
指向堆栈的新的最后一个元素的地址。等价于如下的操作1 2
movq (%rsp), %rax subq $8, %rsp
上面的操作可以保证,%rsp
始终指向栈顶元素的起始地址
条件传送 vs 条件分支
条件分支是我们认识的C语言比较常规的分支程序的处理方式,下面是条件分支的一个例子
计算 $|x- y|$,查看汇编指令,可以看到第8行有一个跳转到L2的过程,这里的问题在于CPU执行程序时候需要根据bool(x<y)判断是否需要跳转,如果判断的bool条件比较复杂,需要经过比较长的时钟周期才可以计算出结果的话,现代CPU不会等待这个结束才继续下面的计算,而是按照分支预测的方式先执行概率比较大的那个分支。如果预测分支执行中,计算得到的bool值跟预期的不一致,就需要丢弃已经执行的分支指令,重新跳转回去执行另一个没有被预测命中的分支,那这样的话计算时间就很长了。
作者介绍了x86下面的传送指令,这些指令会先将源地址S中的数据读取出来,然后检查对应的条件码,根据条件码的值确认是否需要将读出来的值写入到目的寄存器R中,C语言中三目运算符编译器一般会翻译成条件传送指令。
使用传送指令编译之前的计算绝对值的代码,如下所示,
上面代码有2个特点:
- 计算x - y和y - x两个值;
- 没有跳转指令,所以没有之前说的分支预测错误运行时间变长的问题,而且按照现代CPU的逻辑,推理这段代码的ALU单元基本上是满载的。
参考文献
本文原载于 巴巴变的博客,遵循 CC BY-NC-SA 4.0协议,复制请保留原文出处。