我们写程序的目的就是使它在任何情况下都可以稳定工作。一个运行的很快但是结果错误的程序并没有任何用处。在程序开发和优化的过程中,我们必须考虑代码使用的方式,以及影响它的关键因素。通常,我们必须在程序的简洁性与它的运行速度之间做出权衡。今天我们就来聊一聊如何优化程序的性能。
- 1. 减小程序计算量
- 1.1 示例代码
- 1.2 分析代码
- 1.3 改进代码
- 2. 提取代码中的公共部分
- 2.1 示例代码
- 2.2 分析代码
- 2.3 改进代码
- 3. 消除循环中低效代码
- 3.1 示例代码
- 3.2 分析代码
- 3.3 改进代码
- 4. 消除不必要的内存引用
- 4.1 示例代码
- 4.2 分析代码
- 4.3 改进代码
- 5. 减小不必要的调用
- 5.1 示例代码
- 5.2 分析代码
- 5.3 改进代码
- 6. 循环展开
- 6.1 示例代码
- 6.2 分析代码
- 6.3 改进代码
- 7. 累计变量,多路并行
- 7.1 示例代码
- 7.2 分析代码
- 7.3 改进代码
- 8. 重新结合变换
- 8.1 示例代码
- 8.2 分析代码
- 8.3 改进代码
- 9 条件传送风格的代码
- 9.1 示例代码
- 9.2 分析代码
- 9.3 改进代码
- 10. 总结
1. 减小程序计算量
1.1 示例代码
代码语言:javascript复制for (i = 0; i < n; i ) {
int ni = n*i;
for (j = 0; j < n; j )
a[ni j] = b[j];
}
1.2 分析代码
代码如上所示,外循环每执行一次,我们要进行一次乘法计算。i = 0,ni = 0;i = 1,ni = n;i = 2,ni = 2n。因此,我们可以把乘法换成加法,以n为步长,这样就减小了外循环的代码量。
1.3 改进代码
代码语言:javascript复制int ni = 0;
for (i = 0; i < n; i ) {
for (j = 0; j < n; j )
a[ni j] = b[j];
ni = n; //乘法改加法
}
计算机中乘法指令要比加法指令慢得多。
2. 提取代码中的公共部分
2.1 示例代码
想象一下,我们有一个图像,我们把图像表示为二维数组,数组元素代表像素点。我们想要得到给定像素的东、南、西、北四个邻居的总和。并求他们的平均值或他们的和。代码如下所示。
代码语言:javascript复制up = val[(i-1)*n j ];
down = val[(i 1)*n j ];
left = val[i*n j-1];
right = val[i*n j 1];
sum = up down left right;
2.2 分析代码
将以上代码编译后得到汇编代码如下所示,注意下3,4,5行,有三个乘以n的乘法运算。我们把上面的up和down展开后会发现四格表达式中都有i*n j。因此,可以提取出公共部分,再通过加减运算分别得出up、down等的值。
代码语言:javascript复制leaq 1(%rsi), %rax # i 1
leaq -1(%rsi), %r8 # i-1
imulq %rcx, %rsi # i*n
imulq %rcx, %rax # (i 1)*n
imulq %rcx, %r8 # (i-1)*n
addq %rdx, %rsi # i*n j
addq %rdx, %rax # (i 1)*n j
addq %rdx, %r8 # (i-1)*n j
2.3 改进代码
代码语言:javascript复制long inj = i*n j;
up = val[inj - n];
down = val[inj n];
left = val[inj - 1];
right = val[inj 1];
sum = up down left right;
改进后的代码的汇编如下所示。编译后只有一个乘法。减少了6个时钟周期(一个乘法周期大约为3个时钟周期)。
代码语言:javascript复制imulq %rcx, %rsi # i*n
addq %rdx, %rsi # i*n j
movq %rsi, %rax # i*n j
subq %rcx, %rax # i*n j-n
leaq (%rsi,%rcx), %rcx # i*n j n
...
对于GCC编译器来说,编译器可以根据不同的优化等级,有不同的优化方式,会自动完成以上的优化操作。下面我们介绍下,那些必须是我们要手动优化的。
3. 消除循环中低效代码
3.1 示例代码
程序看起来没什么问题,一个很平常的大小写转换的代码,但是为什么随着字符串输入长度的变长,代码的执行时间会呈指数式增长呢?
代码语言:javascript复制void lower1(char *s)
{
size_t i;
for (i = 0; i < strlen(s); i )
if (s[i] >= 'A' && s[i] <= 'Z')
s[i] -= ('A' - 'a');
}
3.2 分析代码
那么我们就测试下代码,输入一系列字符串。
lower1代码性能测试
当输入字符串长度低于100000时,程序运行时间差别不大。但是,随着字符串长度的增加,程序的运行时间呈指数时增长。
我们把代码转换成goto形式看下。
代码语言:javascript复制void lower1(char *s)
{
size_t i = 0;
if (i >= strlen(s))
goto done;
loop:
if (s[i] >= 'A' && s[i] <= 'Z')
s[i] -= ('A' - 'a');
i ;
if (i < strlen(s))
goto loop;
done:
}
以上代码分为初始化(第3行),测试(第4行),更新(第9,10行)三部分。初始化只会执行一次。但是测试和更新每次都会执行。每进行一次循环,都会对strlen调用一次。
下面我们看下strlen函数的源码是如何计算字符串长度的。
代码语言:javascript复制size_t strlen(const char *s)
{
size_t length = 0;
while (*s != '