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 &;应用于每次迭代。
我用一张纸来证实这个理论,首先……
...当然,他是对的——在每次循环迭代中,我们都可以在旧结果的基础上计算出新的结果,通过添加“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个补充!
因此,我继续添加一个循环来多次执行compute
-然后保持执行所需的最小时间:
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倍,其中两个主要的指令只是基于__abc2的添加(addsd
)。第一个版本,不仅有4倍多的指令…它也充满了(正如预期的那样)乘法(mulpd
)。
我承认我没有预料到那个结果。不是因为我是阿格纳的粉丝(我是,但这无关紧要)。
你知道我错过了什么吗?我在这里犯了什么错误,可以解释速度上的差异吗?注意,我已经在至强W5580和至强e5 - 1620上做了测试-在这两个版本中,第一个(哑)版本比第二个快得多。
为了便于重现结果,这两个版本的代码有两个gist: 笨,但不知何故更快和优化了,但不知何故变慢了。
附注:请不要评论浮点精度问题;这不是问题的重点。