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

在读到Mysticial关于如下问题的精彩答案时, 想到了这个问题: 为什么处理sorting后的数组比处理未sorting的数组更快 ?

涉及types的上下文:

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]; 

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

但为什么不这样做呢?

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

希望神秘主义者(或其他任何人)能够给出同样出色的答案。 我从来没有学过在其他问题讨论过的优化,所以我真的很感激。

编译器通常不能转换

 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,产品将变成-1294967296对于具有环绕的典型32位int ,则产品将变成-1294967296 ,而100000次如果没有溢出,则增加30000, sum增加30亿)。 请注意,同样适用于无符号数量,不同的数字,溢出100000 * data[c]通常会引入一个模2^32的减less,不得出现在最终的结果。

它可以转换成

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

尽pipe如此,如果像往常一样, long longint更大。

为什么不这样做呢,我说不出来,我猜这就是Mysticial所说的 ,“很显然,它在循环交换之后并没有运行循环崩溃的通行证”。

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

 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]具有相同的符号,所以如果溢出的话,两者都是这样。

我不太确定编译器是否考虑到了这一点,尽pipe(@Mysticial,你可以尝试使用像data[c] & 0x80左右的条件,对于正值和负值可以是正确的)。 我有编译器进行无效优化(例如,几年前,我有一个ICC(11.0,iirc)在1.0/n中使用signed-32-bit-int-to-double转换,其中n是一个unsigned int 。大约是海湾合作委员会产量的两倍,但错误的是,很多数值大于2^31 ,哎呀)。

这个答案不适用于链接的具体情况,但它适用于问题标题,并可能对未来的读者有意思:

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

 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

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

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

那么,我猜想一些编译器可能会做这种优化,假设我们正在谈论整数算术。

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

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

这种优化有一个概念上的障碍。 编译器作者在减less强度方面花费了大量的精力 – 例如,用乘法和移位来代替乘法。 他们习惯于认为乘数不好。 所以一个应该走另一条路的情况是令人惊讶和违反直觉的。 所以没有人会考虑实施它。

开发和维护编译器的人员花在工作上的时间和精力有限,因此他们通常希望关注用户最关心的内容:将编写良好的代码转换为快速代码。 他们不想花费时间去设法将愚蠢的代码变成快速的代码 – 这就是代码审查的目的。 在高级语言中,可能会有“愚蠢的”代码expression一个重要的想法,值得开发人员花时间去做这个快速的事情 – 例如,砍伐森林和stream式融合的快捷方式使Haskell程序能够围绕某种特定的生成的数据结构被编译成不分配内存的紧密循环。 但是这种激励根本不适用于把循环加法变成乘法。 如果你想要快,就用乘法来写。