我在阅读Agner Fog的优化手册时,看到了这个例子:
double data[LEN];
void compute()
{
const double A = 1.1, B = 2.2, C = 3.3;
int i;
for(i=0; i<LEN; i++) {
data[i] = A*i*i + B*i + C;
}
}
Agner指出,有一种方法可以优化这段代码——通过实现循环可以避免使用昂贵的乘法,而是使用每次迭代应用的“增量”。
我用一张纸来证实这个理论,首先……
...当然,他是对的——在每次循环迭代中,我们都可以根据旧的结果计算出新的结果,通过添加一个“delta”。这个增量从值“A+B”开始,然后在每一步增加“2*A”。
所以我们将代码更新为如下所示:
void compute()
{
const double A = 1.1, B = 2.2, C = 3.3;
const double A2 = A+A;
double Z = A+B;
double Y = C;
int i;
for(i=0; i<LEN; i++) {
data[i] = Y;
Y += Z;
Z += A2;
}
}
就操作复杂性而言,这两个版本的函数确实存在显著差异。在我们的cpu中,与加法相比,乘法的速度要慢得多。我们已经替换了3个乘法和2个加法…只有2个补充!
所以我继续添加一个循环来执行计算很多次-然后保持执行所需的最小时间:
unsigned long long ts2ns(const struct timespec *ts)
{
return ts->tv_sec * 1e9 + ts->tv_nsec;
}
int main(int argc, char *argv[])
{
unsigned long long mini = 1e9;
for (int i=0; i<1000; i++) {
struct timespec t1, t2;
clock_gettime(CLOCK_MONOTONIC_RAW, &t1);
compute();
clock_gettime(CLOCK_MONOTONIC_RAW, &t2);
unsigned long long diff = ts2ns(&t2) - ts2ns(&t1);
if (mini > diff) mini = diff;
}
printf("[-] Took: %lld ns.\n", mini);
}
我编译了两个版本,运行它们…看看这个:
gcc -O3 -o 1 ./code1.c
gcc -O3 -o 2 ./code2.c
./1
[-] Took: 405858 ns.
./2
[-] Took: 791652 ns.
这可真出乎意料。由于我们报告了最小的执行时间,因此我们丢弃了由操作系统各个部分引起的“噪音”。我们还小心地在一台完全不做任何事情的机器上运行。结果或多或少是可重复的-重新运行两个二进制文件显示这是一个一致的结果:
for i in {1..10} ; do ./1 ; done
[-] Took: 406886 ns.
[-] Took: 413798 ns.
[-] Took: 405856 ns.
[-] Took: 405848 ns.
[-] Took: 406839 ns.
[-] Took: 405841 ns.
[-] Took: 405853 ns.
[-] Took: 405844 ns.
[-] Took: 405837 ns.
[-] Took: 406854 ns.
for i in {1..10} ; do ./2 ; done
[-] Took: 791797 ns.
[-] Took: 791643 ns.
[-] Took: 791640 ns.
[-] Took: 791636 ns.
[-] Took: 791631 ns.
[-] Took: 791642 ns.
[-] Took: 791642 ns.
[-] Took: 791640 ns.
[-] Took: 791647 ns.
[-] Took: 791639 ns.
接下来要做的唯一一件事就是看看编译器为这两个版本分别创建了什么样的代码。
objdump -d - s显示了compute的第一个版本——“愚蠢的”,但以某种方式快速的代码——有一个像这样的循环:
第二个优化版本呢?它只增加了两个功能。
我不知道你们怎么想,但就我自己而言,我……困惑。第二个版本的指令大约减少了4倍,其中两个主要的指令只是基于sse的添加(addsd)。第一个版本,不仅有4倍多的指令…它还充满了(正如预期的那样)乘法(mulpd)。
我承认我没有预料到那个结果。不是因为我是阿格纳的粉丝(我是,但这无关紧要)。
你知道我错过了什么吗?我在这里犯了什么错误,可以解释速度上的差异吗?请注意,我已经在Xeon W5580和Xeon E5-1620上进行了测试-在这两个版本中,第一个(哑)版本比第二个版本快得多。
为了更容易地重现结果,这两个版本的代码有两个gist:愚蠢,但在某种程度上更快;优化,但在某种程度上更慢。
附注:请不要评论浮点精度问题;这不是问题的重点。