美文网首页
第五章:优化程序性能

第五章:优化程序性能

作者: 乘瓠散人 | 来源:发表于2021-12-29 19:43 被阅读0次

本章主要介绍了妨碍编译器进行大量优化的因素,比如内存别名使用和过程调用,同时还介绍了循环展开技术,它可以利用现代处理器提供的指令级并行来提高程序的性能。最后,对并行计算的一些基础概念进行了简要介绍。

优化编译器的局限性

内存别名使用

两个过程

两个过程似乎有相同的行为,都是将指针yp指示的位置处的值两次加到指针xp指示的位置处的值。此外,函数twiddle2的效率要更高一些,因为它只要求3次内存引用(读 *xp,读 *yp,写 *xp),而twiddle1需要6次(2次读 *xp,2次读 *yp,2次写 *xp)。

不过,如果两个指针指向同一个内存位置,即xp=yp,那么函数twiddle1实际上将xp的值增加4倍,而函数twiddle2则增加了3倍,这种情况称为内存别名使用。此时twiddle2不能作为twiddle1的优化版本。

如果编译器不能确定两个指针是否指向同一个位置,就必须假设什么情况都有可能,这就限制了可能的优化策略。

过程调用

函数调用

如图所示代码,假设开始时全局变量counter都设置为0,则函数func1会返回6,函数func2会返回0。

这个函数有个副作用——修改了全局程序状态的一部分。大多数编译器不会试图判断一个函数是否有副作用,因此编译器也无法对此做出优化。

消除循环的低效率

消除循环的低效率

上图左侧为原始代码,可以发现循环体每执行一次,都会调用一次函数vec_length,但是数组长度是不变的,因此可以将vec_length移出循环体来提升效率。

减少过程调用

消除循环中的过程调用,结果性能没有明显提升

消除了循环中的函数调用,但是性能没有提升,因为循环中的其他操作形成了瓶颈,限制性能超过调用get_vec_element。

消除不必要的内存引用

combine3的代码将计算的值累积在指针dest指定的位置,通过检查汇编代码,可以发现每次迭代时,累积变量的数值都要从内存读出再写入到内存,这样的读写很浪费,因为每次迭代开始时从dest读出的值就是上次迭代最后写入的值。

把结果累积在临时变量中

现代处理器的性能

近期的Intel处理器是超标量(superscalar)的,意思是它可以在每个时钟周期执行多个操作。此外还是乱序的(out-of-order),意思是指令执行的顺序不一定与机器级程序中的顺序一致。

这样的设计使得处理器能够达到更高的并行度。例如,在执行分支结构的程序时,处理器会采用分支预测技术,来预测是否需要选择分支,同时还预测分支的目标地址。

此外还有一种投机执行(speculative execution)技术,意思是处理器会在分支之前就执行分支之后的操作。如果预测错误,那么处理器就会将状态重置到分支点的状态。

循环展开

循环展开

循环展开是指通过增加每次迭代计算的元素数量来减少循环的迭代次数。如图通过对函数psum1进行循环展开,psum2将所需的迭代次数减半。

循环展开能够从两个方面改进程序的性能:

  • 减少了不直接有助于程序结果的操作的数量,例如循环索引计算和条件分支。
  • 提供了一些方法,可以进一步变化代码,减少整个计算中关键路径上的操作数量。(关键路径就是在循环的反复执行过程中形成的数据相关链)

是否展开的次数越多,性能提升越大?
实际上,循环展开需要维护多个变量,一旦展开次数过多,没有足够的寄存器保存变量,那么就需要将变量保存在内存中,导致访存时间消耗增加。即便在x86-64这样有足够多寄存器的架构中,循环也可能在寄存器溢出之前达到吞吐量限制,从而无法持续提升性能。

并行计算

并行计算机:拥有多个CPU且可同时处理一个问题的机器

指令级并行:同一个CPU内部可以有若干条指令同时执行(流水线)。指令级并行不在用户的控制范围内,而是由编译器和CPU共同决定。

数据级并行:同一操作被并行地应用于许多数据元素。即操作的执行的逻辑相同,只是应用于不同的数据项。

处理器之间通常通过网络连接,由于网络之间传输数据需要时间,因此处理器之间具有通信距离。使用多个处理器意味着额外的通信开销,其次,如果处理器并未分配到完全相同的工作量,则会产生一部分的闲置,造成负载不均衡(load unbalance),降低实际速度。

线程并行:
一个Unix进程对应于单个程序的执行。因此,它在内存中拥有:

  • 程序代码,以机器语言指令的形式存在。
  • 堆(heap),包含malloc创建的数组。
  • 栈,包含快速变化的信息,比如程序计数器(PC),以及具有本地范围的数据项,计算的中间结果。

一个进程可以有多个线程,这些线程的相似之处在于它们看到相同的程序代码和堆,但是它们有自己的
在没有并行硬件的情况下,操作系统将通过多任务或时间切片来处理线程:每个线程定期使用CPU的一小部分时间。这样可以导致更高的处理器利用率,因为一个线程的指令可以在另一个线程等待数据时被处理。

GPU:
图形处理单元(Graphics Processing Unit, GPU)是一种特殊用途的处理器,是为快速图形处理而设计的。CPU受限于内存访问所造成的很长延迟,从而引入了各级缓存,这个限制也适用于GPU。但是GPU采取了不同的方法解决这个问题:GPU关注的是吞吐量计算,以高平均速率提供大量数据,而不是尽可能快地提供任何单一结果。这是通过支持多线程并在它们之间快速切换实现的。当一个线程在等待内存中的数据时,另一个已经拥有数据的线程可以继续进行计算。

GPU依赖于大量的数据并行性快速上下文切换的能力,使得它们在有大量数据并行的图形和科学应用中茁壮成长。

参考:
Datawhale 开源 408 计划——《深入理解计算机系统》

相关文章

网友评论

      本文标题:第五章:优化程序性能

      本文链接:https://www.haomeiwen.com/subject/amuvqrtx.html