csapp archlab Part C

以前做了这部分的,但是分数不高,性能部分只有一半分数,前两天在朋友带动下,热情高涨,重新做了一下,虽然遗憾还是没能满分,最终得分58.6/60.0,不过还是成就感满满~在此分享一下思路吧。

时间关系这里只讲ncopy.ys的设计思路。

lab文档有提示要用第四章的循环展开,嗯,思路便以此为核心(其实如果能重新设计指令的话很容易满分,只是题目不允许)。

1、首先分析初始代码,在前面的部分都能完成的基础上,我们应该很容易看出几个有数据依赖的指令,改变其顺序就行了,然后一些add指令也可以被新设计的iadd替换,这样又可以减少几条指令,感觉性能改进不少,然而,然而这居然是0分!当时我觉得老师真是太难为人了,不过毕竟是cmu嘛。

消除数据依赖产生的气泡,以及换用iaddq

2、接下来只好尝试循环展开了,展开几路是一个值得思考的问题。理论上来讲,在输入数据长度无限大的前提下,循环展开的路数越多,性能就越好,当然代码量也会越大。在满足第一步中的优化前提下,我分别使用了2路,4路,8路(已经接近代码长度上限)循环展开,剩下的余数部分则使用1路循环来完成,得分分别为33.7,41.7,41.3。8路的效果没有我想象的好,看来常量对性能的影响很大,看了一下4路和8路的结果,在输入数据量大的时候明显是8路效果更好,但是在数据量小的时候8路效果不好,确实如此。其实这也跟评分方式有关,小数据量的测试组的权重跟大数据量测试组是一样的,而小数据量计算CPE的时候,常量指令数对其影响非常大。

3、在明确瓶颈所在后,优化的重点就放到了常量部分,如何让常量更快呢?其实常量有个比循环体更大的优势,既然是常量,那我们就没必要依然使用循环,我们把他展开,这样还可以省去每次的两条地址加法,然后每执行一组,num减一,每次判断num是否减为0,如果是,就直接结束。跟循环相比,每次循环需要额外四条指令(两条地址加法,一条num减法,一条结束判断),而这种方法只需要额外两条指令。

再考虑一下余数常量部分应该放到循环前面还是后面,如果是前面的话,计算完余数常量部分之后我们必须把两条地址值加到对应位置,相当于额外多了两条指令,即使是数据量很小根本不会执行后面循环的数据组,也要增加两条指令,而且在前面的话还需要计算出余数的值方便判断结束,综上还是选择在循环后面执行余数常量。

再考虑的一个问题,我们的余数常量部分,每一组都是单个的,肯定是不能随便混到一起的,所以在内存读数据和内存写数据的两条指令中存在数据依赖,可以考虑提前把下一组的内存读指令放到当前组,这样效率会提高很多。针对8路展开,按照上面说的来优化,然后再去掉一些多余的指令,得分可以到56.1。

余数常量部分的展开,以及通过内存读写指令的错位来消除气泡,并去掉了两条地址减法指令

4、最后考虑一种更优的解法,在我们的余数常量指令中,每一组依然需要num减一,然后判断循环结束,这两条指令是否也可以省掉?答案是肯定的,我们可以把余数常量指令按组来倒着写,然后通过判断来跳转到不同的位置,假如余数只剩下1,则跳到最后一组,如果余数是7(8路循环展开),则直接从第一组开始执行。可是如果这样的话,我们似乎没办法通过内存读写指令的错位来去除数据依赖导致的气泡(试试就知道了,确实不行),想了半天突然想到个更加古怪的方法,CPU的条件码是不受内存读写指令或者跳转指令影响的,那我们执行了and指令设置条件码之后,不一定要立即执行条件跳转,因此可以把条件跳转包括%rax加一的两条指令放到下一组操作的两条内存操作指令中间,这样就利用了这个气泡,可新的问题随之而来,如果我们通过前面的判断跳到某一组,那我们就会在这一组多执行一个条件判断,我们第一次的判断是没有意义的,而且这时候的条件码是多少也不知道,如果刚好大于0,则会影响计算结果,因此我们还要保证每次跳转过来之后,条件码的值必须小于等于0,这就是在设计前面的地址跳转要考虑的问题了。

最终,我们要考虑的是在已知余数的值的情况下,如何根据这个值跳转到不同地址,如果按照平时的想法,由于下面每一组的指令长度是一样的,因此可以考虑直接用乘法计算出对应地址,可问题是我们的指令集没有乘法,也没有寄存器间接的跳转指令,这个问题倒是可以解决,乘法无非是一个固定值乘以这个余数对7的加法逆元,用三次加法就可以取代,然后跳转问题我们也可以通过将计算结果放到栈中,然后用ret实现跳转,可事实是这个过程需要的指令数很多(也许是因为我没找到更巧妙的计算方法)。最后我想到的是设计一个三叉搜索树来进行跳转,至于为什么要用三叉树而不是二叉树?实际模拟一下就会发现三叉树的天然优点,条件指令有小于,大于,等于三种,很容易分为三组,而且比起二叉树还能减少一些用于设置条件码的指令,当然还要考虑每次跳转失败带来的损失。

下面这个树结构方案我是在9层(10层分数更高但是代码长度是1009,刚好超了)循环展开的情况下设计的(跟8层比只提高了0.4分),刚好可以满足每次跳转过去的条件码必然是小于等于0的(这个与上面消除气泡的优化结合感觉衔接的还是很巧妙的),没有进行很严谨的分析,遵循了一个大致原则是余数越小则权重越大,因此余数小的尽量减少步骤,余数从0到8(0其实相当于9),用到的步骤分别是11,4,8,8,9,12,13,12,14,也许不是最优,也许可以编程暴力搜索一下最优解,最终分数为58.6,CPE是7.57。

通过将“根据条件码将结果+1”的指令错位,来实现消除气泡
三叉搜索树代码
三叉搜索树图示
运行结果

那么到这里就结束了,我一时想不出完美的方案了,也不想再想了,同时也必须感叹cmu老师们的实力,那个最低CPE7.48一定有着非常精妙的手法。

编辑于 2018-05-08