为什么 Java 中 2*(i*i) 比 2*i*i 更快?

举报
G-washington 发表于 2019/10/29 17:20:24 2019/10/29
【摘要】 有人在 Stack Overflow 上提问,为什么 Java 中的 2 * (i * i) 比 2 * i * i 要快?他做了如下测试。

640?wx_fmt=jpeg&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1


有人在 Stack Overflow 上提问,为什么 Java 中的  2 * (i * i)  比  2 * i * i  要快?

他做了如下测试:

运行下面这段Java代码平均需要0.50到0.55秒:


image.png

如果把2 *(i * i)替换成2 * i * i,那么运行时间在0.60到0.65秒之间。为什么出现这样的结果?

我把程序的每个版本运行了15次,两次之间交替运行。结果如下:

image.png

2 * i * i的最快运行时间比2 * (i * i)最慢运行时间还要长。如果两者效率相当,发生这种情况的可能性小于1/2^15 * 100% = 0.00305%。

来自 rustyx 的回答,获得 1172 赞同

两种方式的字节码顺序略有不同。

2 * (i * i):

image.png

对比2 * i * i:

image.png


乍看之下没有什么不同,如果有的话,第二个版本看起来少了一个slot。

因此,需要更深入研究底层(JIT)。

请记住,对小循环JIT会主动展开。对2 * (i * i)可以看到实际展开了16x:

image.png

image.png

从上面的代码可以看到,有1个寄存器被“spill”到了整个堆栈。

对于2 * i * i版本:

image.png

image.png

image.png

出于保存中间结果的需要,这里出现了更多的“spill”及堆栈[RSP + …]访问。

问题的答案很简单:2 *(i * i)比2 * i * i更快,因为针对前者JIT生成的汇编代码更优化。

但是,显然这两个版本都不够好。由于x86-64 CPU都至少支持SSE2,因此循环可以从向量化中受益。

因此,这是optimizer的问题:通常循环过度展开会带来问题,错失其他优化机会。

实际上,现代x86-64 CPU会把指令进一步细分为微操作(µops)。循环优化可以借助寄存器重命名、µop缓存和循环缓冲区等众多特性,而不是仅仅做一次展开。根据Agner Fog的优化指南:

如果平均指令长度超过4字节,由于µop缓存而导致的性能提升会非常可观。可以考虑下列方法优化µop缓存:

  • 确保关键循环足够小以适应µop缓存。

  • 将最关键的循环条目和功能条目以32对齐。

  • 避免不必要的循环展开。

  • 避免使用需要额外加载时间的指令:..

考虑到加载时间:即使命中最快的L1D也要花费4个周期,需要一个额外的寄存器和µop。只要对存储器访问,哪怕几次也会损害循环的性能。

再考虑矢量化方案:要了解优化能达到多快,可以使用GCC编译类似的C应用程序,直接对其进行矢量化(下面展示了AVX2、SSE2结果):

image.png

运行时间:

  • SSE:0.24 s,大约快2倍。

  • AVX:0.15 s,大约快3倍。

  • AVX2:0.08 s,大约快5倍。

要输出JIT生成的程序集,请获取JVM调试版本,并使用-XX:+ PrintOptoAssembly运行。

C程序版本使用-fwrapv标志进行编译,该标志使GCC可以将带符号整数溢出视为二进制补码。

翻译: 唐尤华
stackoverflow.com/questions/53452713/why-is-2-i-i-faster-than-2-i-i-in-java

本文转载自微信公众号朱小厮的博客

原文链接:https://mp.weixin.qq.com/s/4x8W0IsyqrbdUO3XQDI7Lg

【版权声明】本文为华为云社区用户转载文章,如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。