Chapter 5.2 Optimization Blocker

前面我们提到,当编译器不确定程序员的意图时,它会十分保守地进行优化。所以为了使程序性能更优,程序员自己也需要显式地对程序进行优化。

Procedure Calls

程序中的过程调用会带来意想不到的巨大开销,而且由于编译器的“保守性”,过程调用往往会妨碍大多数形式的程序优化。因此在编写程序的过程中,要减少不必要的过程调用。

下面以一个具体例子来说明,该代码实现了把字符串中所有大写字母变为小写字母:

void lower(char *s)
{
 size_t i;
 for (i = 0; i < strlen(s); i++)
 if (s[i] >= 'A' && s[i] <= 'Z')
 s[i] -= ('A' - 'a');
}

下图展示了随着字符串长度的增加,该函数运行时间的变化。

Lower Case 1 Conversion Performance

很明显,lower函数曲线图随字符串长度的增加上升地很陡峭,它的算法复杂度是\(O(n^{2})\)。这显然是不可接受的。

问题在于,我们实际上每一次循环都调用了strlen这个函数,所以产生了二次的运行时间。但事实上,字符串的长度是固定的,我们只需要在进入循环前算一次就可以了。因此改进代码如下:

void lower(char *s)
{
 size_t i;
 size_t len = strlen(s);
 for (i = 0; i < len; i++)
 if (s[i] >= 'A' && s[i] <= 'Z')
 s[i] -= ('A' - 'a');
}

对比一下改进前后的算法性能:

Lower Case 2 Conversion Performance

这样一来,我们仅仅只是做了这么一个小小的改变,算法的运行时间就变成了线性的,性能得到了显著改进。

Memory Aliasing

内存别名使用(memory aliasing)是指两个指针可能指向同一个内存位置的情况,尽管程序员的本来意图并非如此,但是在保证安全优化的前提下,编译器必须假设不同的指针可能会指向内存中同一个位置。这使得编译器无法做出一些我们期望的优化。此时便又需要程序员显式地优化代码,以提高程序性能。

考虑下面这段代码,它实现了将矩阵a的每一行元素求和存储到向量b中:

void sum_rows1(double *a, double *b, long n) 
{
  long i, j;
  for (i = 0; i < n; i++) 
  {
    b[i] = 0;
    for (j = 0; j < n; j++)
      b[i] += a[i*n + j];
  }
}

对应汇编代码如下:

 # sum_rows1 inner loop
 .L4:
        movsd   (%rsi,%rax,8), %xmm0   # FP load
        addsd   (%rdi), %xmm0          # FP add
        movsd   %xmm0, (%rsi,%rax,8)   # FP store
        addq    $8, %rdi
        cmpq    %rcx, %rdi
        jne     .L4

从汇编中我们可以看出,编译器对b[i]的操作是每次循环都要访问内存进行更新,这是我们非常不想看到的,因为内存操作很慢。产生这个结果的原因就是内存别名使用,编译器必须考虑对b[i]的修改可能对整个程序的内存产生的影响。

因此,当我们去掉内存别名使用,用一个临时变量来替代直接赋值后:

void sum_rows2(double *a, double *b, long n) 
{
  long i, j;
  for (i = 0; i < n; i++) 
  {
    double val = 0;
    for (j = 0; j < n; j++)
      val += a[i*n + j];
    b[i] = val;
  }
}

汇编代码变成了下面的样子:

 # sum_rows2 inner loop
 .L10:
        addsd   (%rdi), %xmm0 # FP load + add
        addq    $8, %rdi
        cmpq    %rax, %rdi
        jne     .L10

是不是清爽多了!现在就不用再存储每一次的值到内存了。因此,在程序中避免内存别名使用也是一个提升程序性能的好方法。


© 2025. ICS Team. All rights reserved.