如果我使用 += 运算符,为什么我的 C 代码会变慢

问题描述 投票:0回答:1

我正在尝试编写一个使用频谱函数对信号进行重新采样的程序。我正在用 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
c performance operator-keyword
1个回答
0
投票

编译器在优化无用代码方面做得很好,但并非总是如此:

  • 如果删除

    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
数组的副作用,因此保持为空。生成的代码被大大简化,并且几乎可以立即执行。

© www.soinside.com 2019 - 2024. All rights reserved.