C 编译器和循环优化

标签 c optimization

对于编译器实际如何优化以及不同级别之间的区别(例如 gcc 的 -O2 与 -O3),我没有太多经验。因此,我不确定以下两个语句对于任意编译器是否等效:

for(i=0;i<10;++i){
variable1*variable2*gridpoint[i];
}

variable3=variable1*variable2;
for(i=0;i<10;++i){
variable3*gridpoint[i];
}

从处理时间的角度来看,只计算一次变量 1 和变量 2 的乘积是有意义的,因为它们在循环中不会改变。但是,这需要额外的内存,而且我不确定优化器对这种开销的影响有多大。如果您有纸质/书籍中的方程式并想将其转换为计算机可读的形式,则第一个表达式是最容易阅读的,但第二个可能是最快的 - 特别是对于循环内有很多未更改变量的更复杂的方程(我有一些非常讨厌的非线性微分方程,我希望代码中的人类可读)。如果我将我的变量声明为常量,这会改变吗?我希望我的问题对任意编译器有意义,因为我同时使用 gcc、Intel 和 Portland 编译器。

最佳答案

对于任意编译器来说,很难充分回答这个问题。使用此代码可以做什么不仅取决于编译器,还取决于目标体系结构。我将尝试解释具有良好功能的生产编译器可以对此代码执行哪些操作。

From a processing-time point of view it would make sense to only compute the product of variable1 and variable2 once as they don't change in the loop.

你是对的。正如 Cat 先生所指出的,这叫做 common subexpression elimination .因此,编译器可能会生成仅计算一次表达式的代码(或者如果已知两个操作数的值一次是常量,则甚至会在编译时计算它)。

如果一个体面的编译器可以确定函数没有副作用,它也可以对函数执行子表达式消除。例如,GCC 可以在函数体可用的情况下分析函数,但也有 pureconst 属性可用于专门标记应受制于的函数此优化(参见 Function Attributes)。

鉴于没有副作用并且编译器能够确定它(在您的示例中,没有任何阻碍),其中两个片段在这方面是等效的(我已经用 clang 进行了检查:-) ).

This requires extra memory however, and I'm not sure how strongly an optimiser factors this overhead in.

事实上,这不需要任何额外的内存。乘法在 processor registers 中完成结果也存储在寄存器中。这是一个消除大量代码并使用单个寄存器存储结果的问题,这总是很棒的(并且在涉及 register allocation 时,尤其是在循环中,这肯定会让生活更轻松)。因此,如果可以完成此优化,则无需额外费用即可完成。

The first expression is the easiest to read..

GCC 和 Clang 都会执行此优化。不过,我不确定其他编译器,因此您必须自己检查一下。但很难想象有哪个好的编译器不进行子表达式消除。

Does any of this change if I declare my variables as constants?

有可能。这称为常量表达式 — 仅包含常量的表达式。可以在编译期间而不是在运行时评估常量表达式。因此,例如,如果您将 A、B 和 C 相乘,其中 A 和 B 都是常量,编译器将根据该预先计算的值预先计算 A*B 表达式,仅乘以 C。如果编译器可以在编译时确定它的值并确保它没有改变,那么即使使用非常量值,编译器也可以这样做。例如:

$ cat test.c
inline int foo(int a, int b)
{
    return a * b;
}

int main() {
    int a;
    int b;
    a = 1;
    b = 2;
    return foo(a, b);
}
$ clang -Wall -pedantic -O4 -o test ./test.c
$ otool -tv ./test
./test:
(__TEXT,__text) section
_main:
0000000100000f70    movl    $0x00000002,%eax
0000000100000f75    ret

对于上述代码段,还可以进行其他优化。以下是我想到的其中一些:

第一个最明显的是循环展开。由于迭代次数在运行时已知,编译器可能决定 unroll the loop .是否应用这种优化取决于架构(即一些 CPU 可以“锁定你的循环”并比其展开版本更快地执行代码,这也通过使用更少的空间使代码更加缓存友好,避免额外的 µOP 融合阶段等)。

第二个可以将速度提高 50 倍的优化是使用 SIMD指令(SSE、AVX 等)。例如,GCC 非常擅长(英特尔也必须如此,如果不是更好的话)。我已经验证了以下功能:

uint8_t dumb_checksum(const uint8_t *p, size_t size)
{
    uint8_t s = 0;
    size_t i;
    for (i = 0; i < size; ++i)
        s = (uint8_t)(s + p[i]);
    return s;
}

... 被转换为一个循环,其中每个步骤一次对 16 个值求和(即在 _mm_add_epi8 中),并使用额外的代码处理对齐和奇数 (<16) 迭代计数。然而,在我最后一次检查时,Clang 完全失败了。因此,即使迭代次数未知,GCC 也可能会以这种方式减少您的循环。

如果可以的话,我建议您不要优化您的代码,除非您发现它是一个瓶颈。否则,您可能会浪费大量时间进行错误和过早的优化。

我希望这能回答您的问题。祝你好运!

关于C 编译器和循环优化,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/14242008/

相关文章:

optimization - SSE 程序在 AMD 上比在 Intel 上花费的时间长得多

c - Linux 上 64 位进程中的地址

c - 二维数组错误

c - 函数指针不兼容的指针类型 void (*)(void *) from void (my_type *)

swift - 为什么 Swift 编译时间这么慢?

ios - 如何优化以获得更好的帧率?

python - 涉及矩阵运算和约束的优化帮助

algorithm - 我的梯度下降算法有什么问题

c - 删除数组的元素

c - 如何使用 c-ldap API 检索整个 LDAP (AD) 目录信息?