CSAPP 读书笔记-第五章优化程序性能

十几年前我看不懂、看不下去,没想到现在还是看不懂、看不下去。第五章把常见的(减少函数调用等)、不常见的(循环展开等)优化手段从指令层面做了深度剖析,授人以鱼的同时也授人以渔,写得真的是好!

但是真得是要花大力气才能理解透彻,要不断地实践加深记忆,才能牢牢掌握这些知识。无奈我一个应用(业务)码农,实在是难得有这种机会,所以只做简单的要点摘要作罢。

编写高效程序需要做到以下几点:第一,我们必须选择一组适当的算法和数据结构。第二,我们必须编写出编译器能够有效优化以转换成高效可执行代码的源代码。

5.4 消除循环的低效率

一个循环天然地带有两个反复执行的语句,所以尽量不要在这两个语句中插入耗时的表达式——函数调用等。

5.5 减少过程调用

也应该尽量把循环体内的函数调用移动到循环外。

5.6 消除不必要的内存引用

循环内诸如 *dest = x + y 的表达式应该优化成 temp = x + y,然后在循环结束把 temp 值传递给 *dest

同理,数组随机访问等等内存寻址行为也应该以临时变量/引用代替。

5.7 理解现代处理器

5.7.2 功能单元的性能

每个运算都是由以下这些数值来刻画的:

  • 延迟(latency),表示完成运算所需要的总时间(执行实际运算所需要的时钟周期总数)
  • 发射时间(issue time),表示两个连续的同类型的运算之间需要的最小时钟周期数(两次运算之间间隔的最小周期数)
  • 容量(capacity),表示能够执行该运算的功能单元的数量(同时能发射多少个这样的操作)

所以如果是串行执行的话,发射时间应该总是大于等于延迟,但通过使用流水线,可以把发射时间减小。流水线化的功能单元实现为一系列的阶段(stage),每个阶段完成一部分的运算。例如,一个典型的浮点加法器包含三个阶段(所以有三个周期的延迟):一个阶段处理指数值,一个阶段将小数相加,而另一个阶段对结果进行舍入。

除法器(用于整数和浮点数除法,还用来计算浮点平方根)不是完全流水线化的——它的发射时间等于它的延迟。除法的长延迟和长发射时间使之成为了一个相对开销很大的运算。

表达发射时间的一种更常见的方法是指明这个功能单元的最大吞吐量,定义为发射时间的倒数。

5.8 循环展开

循环展开能够从两个方面改进程序的性能。首先,它减少了不直接有助于程序结果的操作的数量,例如循环索引计算和条件分支(也就是 for 循环控制语句的后两个语句)。第二,它提供了一些方法,可以进一步变化代码,减少整个计算中关键路径上的操作数量。

“编译器可以很容易地执行循环展开……用优化等级 3 或更高等级调用 GCC,它就会执行循环展开。”

5.9 提高并行性

5.9.1 多个累积变量

在循环展开的基础上,每个循环展开表达式的值赋值给不同的变量,循环结束后再把几个变量的结果合并。

如果前后两个表达式的值都存储在同一个变量里,两次赋值行为就必须串行;如果用两个不同的变量存储表达式值,两个完全无关的语句就有可能并行。

5.11 一些限制因素

5.11.1 寄存器溢出

比如循环展开时,不能无上限地展开,因为寄存器数量是有限的,引入过多的局部变量就导致变量在栈上和寄存器上来回倒,反而变慢。

评论