一、性能分析
我们可以看到,所有需要流水线控制逻辑进行特殊处理的条件,都会导致流水线不能够实现每个时钟周期发射一条新指令的目标。我们可以通过确定往流水线中插入气泡的频率,来衡量这种效率的损失,因为插入气泡会导致未使用的流水线周期。
一条返回指令会产生三个气泡,一个加载/使用冒险会产生一个,而一个预测错误的分支会产生两个。可以通过计算PIPE执行一条指令所需要的平均时钟周期数的估计值,来量化这些处罚对整体性能的影响,这种衡量方法称为CPI(Cydes PerInstruction,每指令周期数)。
可以用如下方法来计算这个基准程序的CPI:
为了估计每种处罚,我们需要知道相关指令(加载、条件转移和返回)的出现频率,以及对每种指令特殊情况出现的频率。对CPI的计算,我们使用下面这组频率:
加载指令(mmovq 和 popq)占所有执行指令的 25%。其中20%会导致加载/使用冒险。
条件分支指令占所有执行指令的20%。其中60%会选择分支,而40%不选择分支。
返回指令占所有执行指令的2%。
因此,我们可以估计每种处罚,它是指令类型频率、条件出现频率和当条件出现时插入气泡数的乘积:
三种处罚的总和是0.27, 所以得到CPI为1.27。
二、未完成的工作
PIPE缺乏的一些实际微处理器设计中所必需的关键特性如下:
1. 多周期指令
Y86-64 指令集中的所有指令都包括一些简单的操作,例如数字加法。这些操作可以在执行阶段中一个周期内处理完。在一个更完整的指令集中,我们还将实现一些需要更为复杂的操作指令,例如,整数乘法和除法,以及浮点运算。在一个像PIPE这样性能中等的处理器中,这些操作的典型执行时间从浮点加法的3或4个周期到整数除法的64个周期。
2. 与存储系统的接口
在对PIPE的描述中,我们假设取指单元和数据内存都可以在一个时钟周期内读或是写内存中任意的位置。但是对于由自我修改代码造成的可能冒险,在自我修改代码中,一条指令对一个存储区域进行写,而后面又从这个区域中读取指令。
进一步说,我们是以存储器位置的虚拟地址来引用它们的,这要求在执行实际的读或写操作之前,要将虚拟地址翻译成物理地址。显然,要在一个时钟周期内完成所有这些处理是不现实的。更糟 糕的是,要访问的存储器的值可能位于磁盘上,这会需要上百万个时钟周期才能把数据读入到处理器内存中。
三、优化程序性能
编写高效程序需要做到以下几点:
第一,我们必须选择一组适当的算法和数据结构。
第二,我们必须编写出编译器能够有效优化以转换成高效可执行代码的源代码。对于这第 二点,理解优化编译器的能力和局限性是很重要的。
1 优化编译器的能力和局限性
编译器必须很小心地对程序只使用安全的优化,也就是说对于程序可能遇到的所有可能的情况,在C语言标准提供的保证之下,优化后得到的程序和未优化的版本有一样的行为。
限制编译器只进行安全的优化,消除了造成不希望的运行时行为的一些可能的原因,但是这也意味着程序员必须花费更大的力气写出编译器能够将之转换成有效机器代码的程序。
上面的没有优化的版本,下面是优化后的版本。
一般情况下,它们都是将存储在由指针yp指示的位置处的值两次加到指针xp指示的位置处的值。但是,考虑xp等于yp的情况,就是地址指向的值是一样的。原来的代码得到4倍结果,而优化后的代码却是3倍,故编译器这样优化会出问题。
因此如果编译器不能确定两个指针是否指向同一个位置,就必须假设什么情况都有可能,这就限制了可能的优化策略。
2 表示程序性
引人度量标准每元素的周期数(CyclesPer Element,CPE),作为一种表示程序性能并指导我们改进代码的方法。CPE这种度量标准帮助我们在更细节的级别上理解迭代程序的循环性能。
处理器活动的顺序是由时钟控制的,时钟提供了某个频率的规律信号,通常用千兆赫兹(GHz), 即十亿周期每秒来表示。
例如,当表明一个系统有 “4GHZ” 处理器,这表示处理器时钟运行频率为每秒4X10的9次方个周期。每个时钟周期的时间是时钟频率的倒数。
3. 消除循环的低效率
代码移动
这类优化包括识别要执行多次(例如在循环里)但是计算结果不会改变的计算。因而可以将计算移动到代码前面不会被多次求值的部分。
这个过程的目的是将一个字符串中所有大写字母转换成小写字母。因为C语言中的字符串是以 null结尾的字符序列,strlen必须一步一步地检查这个序列,直到遇到null字符。对于一个长度为n的字符串,strlen所用的时间与n成正比。因为对lowerl的n次迭代的每一次都会调用strlen,所以 lowerl的整体运行时间是字符串长度的二次项,正比于n的平方。
下面是移动后的代码
把strlen的调用移出了循环以外,这样修改之后,性能有了显著改善。