为什么编译器不能(或不)将可预测的加法循环优化为乘法?

时间:2012-06-30 17:48:48

标签: c performance compiler-optimization

Mysticial问题why is it faster to process a sorted array than an unsorted array阅读精彩答案时,我想到了这个问题:exchanging the loops

所涉及类型的上下文:

const unsigned arraySize = 32768;
int data[arraySize];
long long sum = 0;

在他的回答中,他解释说英特尔编译器(ICC)对此进行了优化:

for (int i = 0; i < 100000; ++i)
    for (int c = 0; c < arraySize; ++c)
        if (data[c] >= 128)
            sum += data[c];

......换成与此相当的东西:

for (int c = 0; c < arraySize; ++c)
    if (data[c] >= 128)
        for (int i = 0; i < 100000; ++i)
            sum += data[c];

优化器正在认识到这些是等价的,因此{{3}}将分支移动到内部循环之外。非常聪明!

但为什么不这样做?

for (int c = 0; c < arraySize; ++c)
    if (data[c] >= 128)
        sum += 100000 * data[c];

希望Mysticial(或任何其他人)可以给出同样出色的答案。我以前从未学过其他问题所讨论的优化,所以我真的很感激。

7 个答案:

答案 0 :(得分:91)

编译器通常无法转换

for (int c = 0; c < arraySize; ++c)
    if (data[c] >= 128)
        for (int i = 0; i < 100000; ++i)
            sum += data[c];

for (int c = 0; c < arraySize; ++c)
    if (data[c] >= 128)
        sum += 100000 * data[c];

因为后者可能导致有符号整数溢出,而前者则没有。即使对带符号的二进制补码整数的溢出保证了环绕行为,它也会改变结果(如果data[c]为30000,则对于典型的32位-1294967296,该乘积将变为int如果没有溢出,将10000次添加30000到sum,将sum增加3000000000)。请注意,对于无符号数量,同样适用于不同的数字,100000 * data[c]的溢出通常会引入一个必须不会出现在最终结果中的缩减模2^32

它可以将其转换为

for (int c = 0; c < arraySize; ++c)
    if (data[c] >= 128)
        sum += 100000LL * data[c];  // resp. 100000ull

但是,如果像往常一样long longint充分大。

为什么它不这样做,我不知道,我猜它是Mysticial said,“显然,它不会在循环交换后运行循环折叠传递”。

请注意,循环交换本身通常不是有效的(对于有符号整数),因为

for (int c = 0; c < arraySize; ++c)
    if (condition(data[c]))
        for (int i = 0; i < 100000; ++i)
            sum += data[c];

会导致溢出

for (int i = 0; i < 100000; ++i)
    for (int c = 0; c < arraySize; ++c)
        if (condition(data[c]))
            sum += data[c];

不会。这是犹太人,因为条件确保添加的所有data[c]具有相同的符号,所以如果一个溢出,两者都有。

我不太确定编译器是否考虑到了这一点(@Mysticial,你能尝试像data[c] & 0x80这样的条件,对于正值和负值都可以这样吗?)。我让编译器进行了无效的优化(例如,几年前,我有一个ICC(11.0,iirc)在1.0/n n中使用了带符号的32位 - 到 - 双 - 转换,其中unsigned int2^31。大约是gcc输出速度的两倍。但错误的是,很多值大于{{1}},oops。)。

答案 1 :(得分:44)

此答案不适用于已关联的特定案例,但它确实适用于问题标题,可能会对未来的读者感兴趣:

由于精度有限,重复浮点加法不等于乘法。考虑:

float const step = 1e-15;
float const init = 1;
long int const count = 1000000000;

float result1 = init;
for( int i = 0; i < count; ++i ) result1 += step;

float result2 = init;
result2 += step * count;

cout << (result1 - result2);

演示:http://ideone.com/7RhfP

答案 2 :(得分:5)

编译器包含执行优化的各种传递。通常在每次传递中,都会对语句或循环优化进行优化。目前还没有基于循环头进行循环体优化的模型。这很难检测,也不太常见。

完成的优化是循环不变代码运动。这可以使用一组技术来完成。

答案 3 :(得分:3)

好吧,我猜一些编译器可能会进行这种优化,假设我们正在讨论整数算术。

同时,一些编译器可能拒绝这样做,因为用乘法替换重复加法可能会改变代码的溢出行为。对于unsigned整数类型,它应该没有区别,因为它们的溢出行为完全由语言指定。但对于签名的那些(可能不是2的补充平台)。确实,签名溢出实际上导致C中的未定义行为,这意味着完全忽略溢出语义应该是完全可以的,并不是所有编译器都足够勇敢地做到这一点。它经常引起很多批评,因为“C只是一种高级汇编语言”的人群。 (还记得当GCC引入基于严格别名语义的优化时发生了什么?)

从历史上看,GCC已经证明自己是一个具有采取如此激烈步骤所需的编译器,但其他编译器可能更愿意坚持所谓的“用户意图”行为,即使语言未定义。

答案 4 :(得分:3)

这种优化存在概念障碍。编译器作者在strength reduction上花了很多精力 - 例如,用乘法和移位替换乘法。他们习惯于认为乘法很糟糕。因此,一个人应该采取另一种方式是令人惊讶和违反直觉的。所以没有人想要实施它。

答案 5 :(得分:0)

开发和维护编译器的人在工作上花费的时间和精力有限,因此他们通常希望专注于用户最关心的事情:将编写良好的代码转换为快速代码。他们不想花时间试图找到将愚蠢的代码转换成快速代码的方法 - 这是代码审查的目的。在高级语言中,可能会有愚蠢的&#34;代码表达了一个重要的想法,使其值得开发人员使用。时间快速 - 例如,短切砍伐森林和流融合允许围绕某些懒惰产生的数据结构构建的Haskell程序被编译成不会分配内存的紧密循环。但是这种激励根本不适用于将循环加法转换为乘法。如果你想要它快,只需用乘法写它。

答案 6 :(得分:0)

现在可以了-at least, clang does

long long add_100k_signed(int *data, int arraySize)
{
    long long sum = 0;

    for (int c = 0; c < arraySize; ++c)
        if (data[c] >= 128)
            for (int i = 0; i < 100000; ++i)
                sum += data[c];
    return sum;
}

用-O1编译到

add_100k_signed:                        # @add_100k_signed
        test    esi, esi
        jle     .LBB0_1
        mov     r9d, esi
        xor     r8d, r8d
        xor     esi, esi
        xor     eax, eax
.LBB0_4:                                # =>This Inner Loop Header: Depth=1
        movsxd  rdx, dword ptr [rdi + 4*rsi]
        imul    rcx, rdx, 100000
        cmp     rdx, 127
        cmovle  rcx, r8
        add     rax, rcx
        add     rsi, 1
        cmp     r9, rsi
        jne     .LBB0_4
        ret
.LBB0_1:
        xor     eax, eax
        ret

整数溢出与它无关。如果存在整数溢出导致不确定的行为,则在两种情况下都可能发生。这是the same kind of function using int instead of long

int add_100k_signed(int *data, int arraySize)
{
    int sum = 0;

    for (int c = 0; c < arraySize; ++c)
        if (data[c] >= 128)
            for (int i = 0; i < 100000; ++i)
                sum += data[c];
    return sum;
}

用-O1编译到

add_100k_signed:                        # @add_100k_signed
        test    esi, esi
        jle     .LBB0_1
        mov     r9d, esi
        xor     r8d, r8d
        xor     esi, esi
        xor     eax, eax
.LBB0_4:                                # =>This Inner Loop Header: Depth=1
        mov     edx, dword ptr [rdi + 4*rsi]
        imul    ecx, edx, 100000
        cmp     edx, 127
        cmovle  ecx, r8d
        add     eax, ecx
        add     rsi, 1
        cmp     r9, rsi
        jne     .LBB0_4
        ret
.LBB0_1:
        xor     eax, eax
        ret