为什么我的C代码在使用+=运算符时会变慢

wooyq4lh  于 11个月前  发布在  其他
关注(0)|答案(2)|浏览(100)

编辑我删除了问题的一部分,因为它可能会令人困惑,感谢答案,我应该能够自己发现。

我试图写一个程序,使用频谱函数对信号进行重采样。我用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

o4hqfura

o4hqfura1#

编译器在优化无用代码方面做得很好,但并不总是这样:

  • 如果你删除for (k = 0; k < 250*6; ++k) tp[k] = ptr[k];这行,你基本上就删除了函数test3()的所有副作用:初始化fxsrf指向的数组,计算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's Compiler Explorer研究编译器的工作,并使用代码和编译器标志。
分析生成的代码可以看出,gccclang都不会为初始化循环生成任何代码(将calloc()分配的内存设置为所有位零值0.0没有效果)并将最后的for循环转换为对memcpy的调用。注解这个最后的for循环会导致两个编译器都不生成代码,将+=更改为=简化了内部循环代码。
有趣的是,尽管两个编译器都正确地确定了fxsrf指向所有元素都等于0.0的数组,但它们并没有假设这些数组的内容在整个计算过程中保持为空,因此目标数组中的所有结果也应该为空,因为ptr[l+p*250] += fx[c+p*2100] * srf[c+l*2100];简化为ptr[l+p*250] += 0.0;
如果将代码编译为C(而不是C++),并将fxsrfptr定义为

double * restrict fx;
    double * restrict srf; 
    double * restrict ptr;


两个编译器都应该确定数组fxsrf不能被修改,因为通过ptr写入的副作用,因此保持为null。生成的代码将大大简化,几乎可以立即执行,但这种优化似乎并不有效。

zbsbpyhn

zbsbpyhn2#

for (k = 0; k < 250*6; ++k) tmp[k] = ptr[k];被删除时,test3没有副作用(不会为程序产生任何结果),而the compiler optimizes main to the two instructions xor eax, eax and ret
+=改为=时,ptr[l+p*250] = fx[c+p*2100] * srf[c+l*2100];ptr[l+p*250]中留下的值只是for (c = 0; c < 2100; ++c)的最后一次迭代的值,因此编译器能够用c为2099的单次迭代替换循环。

相关问题