为什么r=r*(x*(y*z))比r=r*x*y*z的执行效率更高

{% note default %}
r = r * (x * (y * z))和r = r * x * y * z可能看上去本质上是一样的,但是当我们测量CPE的时候,前者要比后者的执行效率更高。
这是为什么呢。这就要说到怎么通过计算机底层的实现机制来进行程序性能的优化了。

理解现代处理器

在此,我们不说现代微处理器的细节。我们要优化程序性能,只需了解处理器设计的两个主要部分:

  1. 指令控制单元(ICU)
  2. 执行单元(EU)
    首先我们应该清楚现代处理器处理指令时指令级并行的,也就是说可以同时执行多条指令。
    那具体怎么实现的呢?

ICU是指取指,然后将其转换成一个或多个操作
EU则是具体去执行ICU传过来的操作

也就是说,ICU就相当于是一个接收指令的长官,长官负责读指令,把它分解成一个或多个任务交给士兵去完成。
而EU就是一系列的执行不同操作的士兵的集合。

理解ICU和EU的协作很重要,往往ICU是在当前执行指令很早之前就取值,所以指令级并行对于拥有数量庞大的士兵的军队来说不是什么难事,因为指令都被分解成操作了,而操作是分别给不同的士兵来完成的,于是多条指令在底层是可以同时运行的。


了解了底层的运作机制,我们就可以探讨如何进行程序的优化了

关键路径

一个程序的执行过程必然有一条关键路径,即在循环的反复过程中所形成的数据相关链。

循环展开

首先我们从浅到深,先讲讲循环展开,我们知道,一个程序的时间性能好坏主要看循环反复执行的代码的效率高不高,而循环展开就是想通过增加每次迭代计算的元素的数量,从而减少循环的迭代次数,达到优化程序性能的效果

for(i = 0; i < limit; i += 2)
{
	acc = (acc OP data[i]) OP data[i + 1];
}

上述代码(2×1循环展开)每次循环处理数组的两个元素。也就是每次迭代,循环索引i加2,在一次迭代中,对数组元素i和i+1使用合并运算。

提高并行性

这个时候可能你会想到,既然可以循环展开,那么我可不可以利用指令级并行,将我要完成的目标分解成两个或更多的部分来同时进行?最后只需把结果合并就是了。
因此k×k循环展开就是将循环展开k次,以及并行累计k个值。于是我们的程序就有多条关键路径,每条路径需要执行的操作数量更少了,但是我们的所有关键路径都是同时进行的,也就是说,我们利用了并行,使得效率更高了。

重新结合变换

回到开头,为什么r = r * (x * (y * z))和r = r * x * y * z的执行效率不一样呢。
通过我们对关键路径以及一条指令可分成多个操作的理解,不难想象,如果我们没有重新结合变换(r = r * (x * (y * z))),即是从左到右顺序操作,先是r*x,然后再是(r * x) * y,最后是((r * x) * y) * z。有没有发现每次都是我前一个操作执行完了,我后一个操作才能执行,也就是说后一个操作必须等前一个操作计算完了才能执行,这样我的关键路径在每一次迭代的时候就必须包含3个操作(不是底层意义上的操作),现在我就想减少每一次迭代的关键路径上的操作数量,也就是说我后面的某些操作不需要前提条件就能执行,更通俗得讲,我不想等水烧开了我再去把面找出来
于是,
r = r * ((x * y) * z):












性能提高技术

在这里总结一下优化程序性能的策略:

  1. 高级设计。为遇到的问题选择适当的算法和数据结构。要特别警觉,避免使用那些会渐进产生糟糕性能的算法或编码技术。
  2. 基本编码原则。避免限制优化的因素
  • 消除连续的函数调用。在可能时,将计算移到循环外。考虑有选择地妥协程序的模块性以获得更大的效率。
  • 消除不必要的内存引用。引入临时变量来保存中间结果。只有在最后的值计算出来时,才将结果存放在数组或全局变量中。
  1. 低级优化
  • 展开循环,降低开销,并且使得进一步的优化成为可能。
  • 通过使用例如多个累积变量和重新结合等技术,找到方法提高指令级并行。
    用功能性的风格写条件操作,使得编译采用条件数据传送

后期我会拓展并更新,并相应配图。


posted @ 2022-01-06 23:49  Ryan~~~~  阅读(147)  评论(0)    收藏  举报