我正在尝试编写一个使用频谱函数对信号进行重新采样的程序。我正在用 C 编写代码,但我发现用 C++ 编码存在问题。
这是我正在使用的代码的一部分。我有一个函数
test()
调用另一个函数 test3(tmp)
将数组作为指针传递。只要我组合 +=
运算符并且尝试填充第一个函数的数组,我的代码就会变慢。
#include <stdio.h>
#include <stdlib.h>
void test3(double *tmp);
void test() {
double *tmp;
int k;
tmp = (double *) calloc(250 * 6, sizeof(double));
for (k = 0; k < 1000; ++k)
test3(tmp);
free(tmp);
}
void test3(double *tmp) {
int k;
double *fx;
double *srf;
double *ptr;
int p;
int l;
int c;
double a;
fx = (double *) calloc(2100 * 6, sizeof(double));
srf = (double *) calloc(2100 * 250, sizeof(double));
for (int k = 0; k < 2100 * 6; ++k) fx[k] = 0;
for (int k = 0; k < 2100 * 250; ++k) srf[k] = 0;
ptr = (double *) calloc(250 * 6, sizeof(double));
for (p = 0; p < 6; ++p) {
for (l = 0; l < 250; ++l) {
ptr[l + p*250] = 0.0;
for (c = 0; c < 2100; ++c)
ptr[l+p*250] += fx[c+p*2100] * srf[c+l*2100];
}
}
for (k = 0; k < 250 * 6; ++k) tmp[k] = ptr[k];
free(ptr);
free(fx);
free(srf);
}
int main(int argc, char *argv[]) {
test();
return 0;
}
我编译使用:
gcc -O2 -o test test.c -lm
当我运行代码时
time ./test
我得到了
./test_lut 3.84s user 0.11s system 98% cpu 4.008 total
现在如果我删除这条线
for (k = 0; k < 250*6; ++k) tmp[k] = ptr[k];
我得到了
./test_lut 0.00s user 0.00s system 3% cpu 0.193 total
更奇怪的是,如果我保留原始代码但将
+=
更改为 =
,我得到
./test_lut 0.07s user 0.14s system 46% cpu 0.450 total
我不明白为什么我的程序变慢了。我做错了什么。
我在函数中尝试了以下操作
test3()
double a;
for (p = 0; p < 6; ++p) {
for (l = 0; l < 250; ++l) {
a = 0.0;
for (c = 0; c < 2100; ++c)
a += fx[c+p*2100] * srf[c+l*2100];
ptr[l+p*250] = a;
}
}
for (k = 0; k < 250*6; ++k) tp[k] = ptr[k];
再说一遍,这很慢。
./test_lut 3.84s user 0.12s system 94% cpu 4.171 total
编译器在优化无用代码方面做得很好,但并非总是如此:
如果删除
for (k = 0; k < 250*6; ++k) tp[k] = ptr[k];
行,则基本上删除了函数 test3()
的所有副作用:初始化 fx
和 srf
指向的数组,并计算 ptr
指向的数组中的条目,然后释放这些数组。编译器似乎能够确定从函数返回后没有幸存的对象受到影响,并完全删除代码,从而导致几乎不花时间在函数中。
如果将
+=
更改为 =
,循环
for (c = 0; c < 2100; ++c)
ptr[l+p*250] += fx[c+p*2100] * srf[c+l*2100];
不断修改相同的位置,因此只有最后一次迭代是有用的,因此代码减少到
ptr[l+p*250] += fx[2099+p*2100] * srf[2099+l*2100];
,从而导致运行速度更快。
第三次尝试使用临时变量
a
没有影响,因为编译器可能已经优化了循环外的常量目标。
您可以使用 Godbolt 的编译器资源管理器 研究编译器工作,并使用代码和编译器标志。
分析生成的代码可以看出,近来 gcc 不会 clang 生成初始化循环的任何代码(将
calloc()
分配的内存设置为所有位零值 0.0
没有效果)。注释最后的 for
循环会导致两个编译器不生成任何代码,并将 +=
更改为 =
可以简化内部循环代码。
有趣的是,尽管两个编译器都正确地确定
fx
和 srf
指向所有元素都等于 0.0
的数组,但它们并不假设这些数组的内容在整个计算过程中保持为空,因此所有结果都是目标数组也应该为空。
如果将代码编译为 C 并将
fx
和 srf
定义为
double * restrict fx;
double * restrict srf;
两个编译器都会知道数组不能被修改,这是写入
ptr
数组的副作用,因此保持为空。生成的代码被大大简化,并且几乎可以立即执行。