java - 编译器使用关联性有什么问题?

标签 java c performance gcc optimization

有时关联性可用于松散数据依赖性,我很好奇它能提供多大帮助。我很惊讶地发现,通过手动展开一个简单的循环,我几乎可以将速度提高 4 倍,无论是在 Java (build 1.7.0_51-b13) 还是在 C (gcc 4.4) 中.3).

所以要么我在做一些非常愚蠢的事情,要么编译器忽略了一个强大的工具。我开始于

int a = 0;
for (int i=0; i<N; ++i) a = M1 * a + t[i];

它计算的东西接近于 String.hashCode()(设置 M1=31 并使用 char[])。计算非常简单,对于 t.length=1000,我的 i5-2400 @ 3.10GHz(在 Java 和 C 中)大约需要 1.2 微秒。

观察每两个步骤 a 乘以 M2 = M1*M1 并添加一些东西。这就引出了这段代码

int a = 0;
for (int i=0; i<N; i+=2) {
    a = M2 * a + (M1 * t[i] + t[i+1]); // <-- note the parentheses!
}
if (i < len) a = M1 * a + t[i]; // Handle odd length.

这比第一个片段快两倍。奇怪的是,省略括号会减少 20% 的加速。有趣的是,这可以重复进行,并且可以达到 3.8 的系数。

与 java 不同,gcc -O3 选择不展开循环。这是明智的选择,因为无论如何它都无济于事(如 -funroll-all-loops 所示)。

所以我的问题1是:是什么阻碍了这种优化?

谷歌搜索无效,我只得到“关联数组”和“关联运算符”。

更新

我擦亮了我的 benchmark一点点,可以提供一些results现在。除了展开 4 次之外没有任何加速,可能是因为乘法和加法一起需要 4 个周期。

更新2

由于 Java 已经展开了循环,所以所有艰苦的工作都已完成。我们得到的是这样的

...pre-loop
for (int i=0; i<N; i+=2) {
    a2 = M1 * a + t[i];
    a = M1 * a2 + t[i+1];
}
...post-loop

有趣的部分可以像这样重写

    a = M1 * ((M1 * a) + t[i]) + t[i+1]; // latency 2mul + 2add

这表明有 2 个乘法和 2 个加法,所有这些都按顺序执行,因此在现代 x86 CPU 上需要 8 个周期。我们现在需要的只是一些小学数学(即使在溢出或其他情况下也适用于 int,但不适用于 float )。

    a = ((M1 * (M1 * a)) + (M1 * t[i])) + t[i+1]; // latency 2mul + 2add

到目前为止我们一无所获,但它允许我们折叠常量

    a = ((M2 * a) + (M1 * t[i])) + t[i+1]; // latency 1mul + 2add

通过重新组合和获得更多

    a = (M2 * a) + ((M1 * t[i]) + t[i+1]); // latency 1mul + 1add

最佳答案

以下是我对您的两种情况的理解:在第一种情况下,您有一个需要 N 步的循环;在第二种情况下,您手动将第一种情况的两个连续迭代合并为一个,因此在第二种情况下您只需要执行 N/2 个步骤。你的第二个案例运行得更快,你想知道为什么愚蠢的编译器不能自动完成它。

没有什么可以阻止编译器进行此类优化。但请注意,这种对原始循环的重写会导致更大的可执行文件大小:您for 内有更多说明循环和额外的 if循环之后。
如果 N=1 或 N=3,原始循环可能会更快(更少的分支和更好的缓存/预取/分支预测)。它使事情变得更快在你的情况下但它可能使事情在其他情况下变慢。是否值得进行此优化尚不清楚,在编译器中实现此类优化可能非常重要

顺便说一句,您所做的与循环矢量化非常相似,但在您的情况下,您手动执行了并行步骤并插入了结果。埃里克布鲁默的 Compiler Confidential谈话会让您了解为什么重写循环通常很棘手,以及有哪些缺点/缺点(更大的可执行文件大小,在某些情况下可能更慢)。因此,编译器编写者非常清楚这种优化的可能性,并正在积极致力于此,但它通常非常重要,并且还会使事情变得更慢。


请为我尝试一些东西:

int a = 0;
for (int i=0; i<N; ++i) 
  a = ((a<<5) - a) + t[i];

假设M1=31 .原则上,编译器应该足够聪明来重写 31*a进入(a<<5)-a但我很好奇它是否真的这样做了。

关于java - 编译器使用关联性有什么问题?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/21745619/

相关文章:

java - Java中父类(super class)引用无法调用子类方法

java - 升级 spring 依赖项后出现错误 A JTA EntityManager cannot use getTransaction()

java - 如何从泛型中获取类类型信息?

c - 在 OpenGL 中使用箭头键移动一些形状 - 它会缩小而不是移动

c - scanf() 是否将 '\n' 作为前一个 scanf() 的输入剩余?

python - 为什么 numpy 比 python 慢?如何让代码执行得更好

c# - 在循环迭代之间执行代码

java - Handler.removeCallbacks() 不会删除回调 - 为什么?

python - C 中的字符串长度评估

mysql - MySQL中多列索引的字段顺序是否重要