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

jha*_*ott 126 c performance compiler-optimization

在阅读Mysticial对问题的精彩回答时,这个问题浮现脑海中:为什么处理排序数组比处理未排序数组更快

涉及类型的上下文:

const unsigned arraySize = 32768;
int data[arraySize];
long long sum = 0;
Run Code Online (Sandbox Code Playgroud)

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

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

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

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

优化器认识到这些是等效的,因此交换循环,将分支移动到内循环之外.非常聪明!

但为什么不这样做呢?

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

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

Dan*_*her 95

编译器通常无法转换

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

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

因为后者可能导致有符号整数溢出,而前者则没有.即使保证了带符号的二进制补码整数溢出的环绕行为,它也会改变结果(如果data[c]是30000,产品将成为-1294967296典型的32位ints并带有环绕,而100000次将添加30000 sum,如果是不溢出,增加sum3000000000).注意,同样适用于无符号数量,具有不同的数字,溢出100000 * data[c]通常会引入减少模数2^32,该模数不得出现在最终结果中.

它可以将其转化为

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

但是,如果像往常一样,long long是否足够大int.

为什么它不这样做,我不知道,我猜这是Mysticial所说的,"显然,它不会在循环交换之后运行循环崩溃的传递".

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

for (int c = 0; c < arraySize; ++c)
    if (condition(data[c]))
        for (int i = 0; i < 100000; ++i)
            sum += data[c];
Run Code Online (Sandbox Code Playgroud)

可以导致溢出的地方

for (int i = 0; i < 100000; ++i)
    for (int c = 0; c < arraySize; ++c)
        if (condition(data[c]))
            sum += data[c];
Run Code Online (Sandbox Code Playgroud)

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

我不太确定编译器是否考虑到了这一点(@Mysticial,你可以尝试使用类似的条件data[c] & 0x80,对于正值和负值都可以这样做吗?).我让编译器进行了无效的优化(例如,几年前,我有一个ICC(11.0,iirc)使用signed-32-bit-int-to-double转换,1.0/n其中na unsigned int.的速度大约是gcc的两倍.输出.但错了,很多值都比2^31oops大.)

  • 我记得MPW编译器的一个版本,它添加了一个允许大于32K的堆栈帧的选项[早期版本受限于使用@ A7 + int16寻址局部变量].它适用于低于32K或超过64K的堆栈帧,但对于40K堆栈帧,它将使用`ADD.W A6,$ A000`,忘记带地址寄存器的字操作符号 - 在添加前将字扩展为32位.花了一些时间进行故障排除,因为代码在"ADD"和下次从堆栈弹出A6之间唯一的做法是恢复它保存到该帧的调用者寄存器...... (3认同)
  • ...并且调用者唯一关心的寄存器是静态数组的[load-time constant]地址.编译器知道数组的地址保存在寄存器中,因此可以根据它进行优化,但调试器只知道常量的地址.因此,在语句`MyArray [0] = 4之前;`我可以检查`MyArray`的地址,并在执行语句之前和之后查看该位置; 它不会改变.代码类似于`move.B @ A3,#4`和A3应该在指令执行的任何时候始终指向"MyArray",但事实并非如此.乐趣. (3认同)

Ben*_*igt 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);
Run Code Online (Sandbox Code Playgroud)

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

  • @nightcracker:StackOverflow的既定目标是构建一个对未来用户有用的可搜索的答案库.这是对所提问题的答案......恰好有一些未说明的信息使得这个答案不适用于原始海报.它仍然适用于具有相同问题的其他人. (28认同)
  • 它_could_是问题__title__的答案,但不是问题,不是. (12认同)
  • 这个问题没有答案.尽管有趣的信息(并且必须知道任何C/C++程序员),但这不是论坛,也不属于此. (10认同)
  • 正如我所说,这是__有趣的信息.然而,对于我来说,似乎不应该回答_问题的答案,而不是回答现在的问题.这根本不是英特尔编译器决定不优化的原因,basta. (6认同)
  • @nightcracker:我觉得这也是最好的答案.我希望有人能为整数案例发布一个非常好的答案,这个案例在得分方面超过了这个.不幸的是,我不认为对于整数情况有"不能"的答案,因为转换是合法的,所以我们留下"为什么它没有",这实际上与"过于本地化的"接近原因,因为它是特定编译器版本所特有的.我回答的问题是更重要的一个,IMO. (4认同)
  • @nightcracker:问题并没有说'data`是一个整数还是浮点类型.第三方编辑了. (3认同)

Jas*_*n S 6

现在确实如此——至少 clang 确实如此

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;
}
Run Code Online (Sandbox Code Playgroud)

使用 -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
Run Code Online (Sandbox Code Playgroud)

整数溢出与之无关;如果存在导致未定义行为的整数溢出,则在任何一种情况下都可能发生。这是使用而不是相同类型的函数intlong

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;
}
Run Code Online (Sandbox Code Playgroud)

使用 -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
Run Code Online (Sandbox Code Playgroud)


kni*_*der 5

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

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