1.1 示例代码
- for (i = 0; i < n; i++) {
- int nni = 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 改进代码
- 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 示例代码
想象一下,我们有一个图像,我们把图像表示为二维数组,数组元素代表像素点。我们想要得到给定像素的东、南、西、北四个邻居的总和。并求他们的平均值或他们的和。代码如下所示。
- 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等的值。
- 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 改进代码
- long iinj = 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个时钟周期)。
- 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编译器来说,编译器可以根据不同的优化等级,有不同的优化方式,会自动完成以上的优化操作。下面我们介绍下,那些必须是我们要手动优化的。