Lec 2 Bentley 程序优化的法则
算法设计可以显著减少解决问题所需的工作量,例如用 Θ(n log n) 时间复杂度的排序替代 Θ(n²) 时间复杂度的排序。 然而,减少程序的工作量并不一定会自动减少其运行时间,这是由于计算机硬件的复杂特性,包括
- 指令级并行(ILP)
- 缓存
- 向量化
- 推测执行和分支预测等等
尽管如此,减少工作量仍然是减少整体运行时间的一个有效启发式方法。
我们这门课的的优化是,是与体系结构相关的优化。
总览

- 数据结构
- 循环
- 逻辑
- 函数
数据结构
打包和编码
打包(Packing)的思路是将多个数据值存储在一个机器字中,而编码(coding)的想法是数据值转换为需要更少位数的表示方式。
示例:编码日期
字符串形式的日期为“2018年9月11日”会占用 18 个字节,超过两个 64 位(double word)的机器字大小。每当我们操作日期时,必须移动这些字节,增加了开销。假设我们只需要存储从公元前 4096 年到公元 4096 年之间的日期。这大约有 365.25 × 8192 ≈ 300 万个日期。使用二进制表示时,编码这些日期大约需要 ⎡log₂(3 × 10⁶)⎤ ≈ 22 位,因此可以轻松地存储在一个 32 位的机器字中
如何紧凑地表示日期,以便快速确定年、月、日?
Solution: 我们可以通过位域(bit-field)方式,把日期的 year/month/day 打包到一个结构体中,只用 22 位就能表示,而且访问单个字段(例如只取出 month)也非常快,因为编译器会生成高效的访问代码。
增强
增强(Augmentation)数据结构是指,通过向数据结构中添加额外信息,使常见操作所需的工作量减少,从而提高操作效率。
示例:单链表拼接。假设我们有两个单链表,要将一个链表(A)拼接到另一个链表(B)的末尾。由于单链表只有指向下一个节点的指针,没有直接指向尾部的指针,因此我们需要遍历链表 A 的所有节点,直到找到它的末尾(空指针位置),然后将这个空指针指向链表 B 的开头。这个过程的时间复杂度是 O(n),其中 n 是链表 A 的长度。
增强链表:如果我们在链表结构中增加一个尾指针(tail pointer),直接指向链表的最后一个节点,那么拼接操作就可以直接找到链表 A 的末尾,无需遍历整个链表。这样拼接操作可以在常数时间(O(1))内完成,大大提高效率

预计算
预计算(Precomputation)的思想是提前进行计算,以避免在“任务关键”的时刻进行计算。
示例:二项式系数。
💡关键思想:在程序初始化时提前计算出一张二项式系数表(比如一个二维数组),在运行时通过查表直接获取结果,而不再进行实时计算。
第一步: Pascal's 三角

int choose(int n, int k) {
if (n < k) return 0;
if (k == 0) return 1;
return choose(n-1, k-1) + choose(n-1, k);
}第二步: 预计算Pascal
#define CHOOSE_SIZE 100
int choose[CHOOSE_SIZE][CHOOSE_SIZE];
void init_choose() {
for (int n = 0; n < CHOOSE_SIZE; ++n) {
choose[n][0] = 1;
choose[n][n] = 1;
}
for (int n = 1; n < CHOOSE_SIZE; ++n) {
choose[0][n] = 0;
for (int k = 1; k < n; ++k) {
choose[n][k] = choose[n-1][k-1] + choose[n-1][k];
choose[k][n] = 0;
}
}
}编译期间初始化
编译时初始化的思想是在编译时存储常量的值,从而在程序执行时节省工作量。
#define N 100
int main(int argc, const char *argv[]) {
init_choose();
printf("#define N %3d\n”, N);
printf("int choose[N][N] = {\n");
for (int a = 0; a < N; ++a) {
printf(" {");
for (int b = 0; b < N; ++b) {
printf("%3d, ", choose[a][b]);
}
printf("},\n");
}
printf("};\n");
}元编程(metaprogramming)是对程序进行编程的程序,可以通过这种技巧
缓存
缓存(cache)的思想是将最近访问过的结果存储起来,以便程序不必再次计算它们。

稀疏性
稀疏性的思想是避免存储和计算零值。简而言之,就是“最快的计算方式是不进行计算
示例: CSR(压缩稀疏行矩阵表示法)
逻辑
循环
循环不变量外提
循环不变量外提(Hoisting)是一种编译器优化技术,目的是将循环内部不变的计算(Loop-Invariant Code)移到循环外部,避免在每次迭代中重复计算相同值,从而提升性能

循环融合
循环融合(Loop Fusion, 也称Loop Jamming)是一种编译器优化技术,目的是将多个相邻的、遍历相同索引范围的循环合并为一个循环,从而减少循环控制的开销(如循环变量更新、条件判断等),并提升数据局部性。

消除无效迭代
消除无效迭代(Eliminating Wasted Iterations)的思想是,通过调整循环边界(loop bounds),跳过那些循环体内实际没有有效操作的迭代

函数
内联
内联(Inlining)的关键思想子阿姨,减少函数调用开销,用函数体替换调用。

也可以通过将square声明为内联函数,内联函数有着比肩宏(macros)的性能,并且他们更安全和更好的结构化(宏只是简单的替换)。
尾递归消除
尾递归消除(Tail-Recursion Elimination)的理念是消除函数最后一步递归调用的开销。最后一步的递归调用被替换为跳转到函数开头,更新参数的值(即复用栈帧)

粗化递归
粗化递归(Coarsening Recursion)的理念是增加基准条件的大小,并使用更高效的代码来处理它,从而避免函数调用的开销。
