【问题标题】:Does Java optimize division by powers of two to bitshifting?Java是否优化了二的幂除以移位?
【发布时间】:2013-09-04 19:58:32
【问题描述】:

Java 编译器 JIT 编译器是否将除法或乘法优化为以 2 的恒定幂向下移动?

例如,以下两个语句是否优化为相同?

int median = start + (end - start) >>> 1;
int median = start + (end - start) / 2;

(基本上是this question 但对于Java)

【问题讨论】:

  • 这两条语句生成的字节码你看了吗?
  • 请注意,有几个编译器。例如 javac 和 eclipse 中的那个。
  • @Julien 我也在考虑 JIT。
  • @WChargin:没错。查看字节码并找到除以 2,并不意味着 JIT 不能做不同的事情。
  • javac 编译器几乎不做任何优化。只有 JIT 可以这样做,但我认为情况并非如此,因为操作不一样。

标签: java optimization compiler-construction javac


【解决方案1】:

不,Java 编译器不这样做,因为它不能确定(end - start) 的符号是什么。为什么这很重要?负整数的位移产生与普通除法不同的结果。在这里你可以看到一个演示:this simple test:

System.out.println((-10) >> 1);  // prints -5
System.out.println((-11) >> 1);  // prints -6
System.out.println((-11) / 2);   // prints -5

另外请注意,我使用了>> 而不是>>>>>> 是无符号位移,而 >> 是有符号的。

System.out.println((-10) >>> 1); // prints 2147483643

@Mystical:我写了一个基准测试,显示编译器/JVM 没有做那个优化:https://ideone.com/aKDShA

【讨论】:

  • 一个除法肯定比3个基本指令慢。根据您要划分的内容,它们往往在 10 到 70 个周期之间。而大多数基本指令只有 1 个周期。 (不计算吞吐量)
  • 这里是核心细节:agner.org/optimize/instruction_tables.pdf 你不需要删除你的答案。编译器不能直接将其优化为单班是正确的。我只是说它可能会做其他事情。
  • 哇。这是一个惊喜!我的 C++ 编译器会这样做。我原以为 JIT 能够像这样进行“基本”优化。我说“基本”,因为虽然它对程序员来说不一定是基本的(而且你不应该手动做一些事情),但它被认为是编译器的基本。
  • 您的测试只是证明优化器在短时间内根本没有发挥作用。由于您的测试代码不使用计算结果,Hotspot 将完全删除计算,而不是用班次或类似的方式替换它(一旦它开始工作)。这反过来又允许完全删除循环,因此您肯定会注意到优化器何时启动。如果你测量的时间超过几纳秒,它就没有……
  • 现在您的基准测试显示出巨大的差异:“运行 1:295044177,运行 2:3749731100”。然而,它坏得很厉害。请用 JMH 或 caliper benchmark 替换它(我的回答)或修复它 w.r.t.死代码消除和其他陷阱。
【解决方案2】:

虽然在除法不能简单地用右移代替的意义上,公认的答案是正确的,但基准是非常错误的。 任何运行时间少于一秒的 Java 基准测试都可能衡量解释器的性能——这不是你通常关心的。

我忍不住写了一个自己的benchmark,主要表明这一切都更加复杂。我并不想完全解释results,但我可以这么说

  • 一般师的操作太慢了
  • 尽量避免
  • 除以常数得到 AFAIK 总是以某种方式优化
  • 除以 2 的幂被右移和负数调整所取代
  • 手动优化的表达式可能会更好

【讨论】:

  • 您的答案是“除以 2 的幂被右移和负数的调整所取代”,而接受的答案说它没有那么它是哪一个?
  • @vach 我的基准测试清楚地表明优化确实完成了(但是,取决于 VM 和 CPU)。由于消除了死代码,已接受答案的基准完全被打破,所以我们可以忘记它(如果您不确定该信任谁,请阅读 JMH blackhole)。
【解决方案3】:

如果JVM不做,你可以自己轻松做。

如上所述,负数的右移与除法的行为不同,因为结果以错误的方向四舍五入。如果您知道股息是非负的,则可以安全地用班次替换除法。如果它可能是负面的,您可以使用以下技术。

如果你可以用这种形式表达你的原始代码:

int result = x / (1 << shift);

你可以用这个优化的代码替换它:

int result = (x + (x >> 31 >>> (32 - shift))) >> shift;

或者,或者:

int result = (x + ((x >> 31) & ((1 << shift) - 1))) >> shift;

这些公式通过添加一个从被除数的符号位计算得出的小数来补偿不正确的舍入。这适用于所有移位值从 1 到 30 的任何 x

如果移位为 1(即除以 2),则可以在第一个公式中删除 &gt;&gt; 31 以提供非常整洁的 sn-p:

int result = (x + (x >>> 31)) >> 1;

我发现这些技术即使在非恒定偏移时也更快,但显然如果偏移是恒定的,它们会受益最大。注意:对于 long x 而不是 int,将 31 和 32 分别更改为 63 和 64。

Examining the generated machine code 表明(不出所料)HotSpot Server VM 可以在班次不变时自动执行此优化,但(同样不出所料)HotSpot Client VM 太笨了。

【讨论】:

    猜你喜欢
    • 2018-07-24
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2013-08-06
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多