点击上方“朱小厮的博客”,选择“设为星标”

回复”666“获取公众号专属资料

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

他做了如下测试:

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

public static void main(String[] args) {long startTime = System.nanoTime();int n = 0;for (int i = 0; i < 1000000000; i++) {n += 2 * (i * i);}System.out.println((double) (System.nanoTime() - startTime) / 1000000000 + " s");System.out.println("n = " + n);
}

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

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

2*(i*i)  |  2*i*i
----------+----------
0.5183738 | 0.6246434
0.5298337 | 0.6049722
0.5308647 | 0.6603363
0.5133458 | 0.6243328
0.5003011 | 0.6541802
0.5366181 | 0.6312638
0.515149  | 0.6241105
0.5237389 | 0.627815
0.5249942 | 0.6114252
0.5641624 | 0.6781033
0.538412  | 0.6393969
0.5466744 | 0.6608845
0.531159  | 0.6201077
0.5048032 | 0.6511559
0.5232789 | 0.6544526

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

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

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

2 * (i * i):

     iconst_2iload0iload0imulimuliadd

对比2 * i * i:

     iconst_2iload0imuliload0imuliadd

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

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

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

030   B2: # B2 B3 <- B1 B2  Loop: B2-B2 inner main of N18 Freq: 1e+006
030     addl    R11, RBP    # int
033     movl    RBP, R13    # spill
036     addl    RBP, #14    # int
039     imull   RBP, RBP    # int
03c     movl    R9, R13 # spill
03f     addl    R9, #13 # int
043     imull   R9, R9  # int
047     sall    RBP, #1
049     sall    R9, #1
04c     movl    R8, R13 # spill
04f     addl    R8, #15 # int
053     movl    R10, R8 # spill
056     movdl   XMM1, R8    # spill
05b     imull   R10, R8 # int
05f     movl    R8, R13 # spill
062     addl    R8, #12 # int
066     imull   R8, R8  # int
06a     sall    R10, #1
06d     movl    [rsp + #32], R10    # spill
072     sall    R8, #1
075     movl    RBX, R13    # spill
078     addl    RBX, #11    # int
07b     imull   RBX, RBX    # int
07e     movl    RCX, R13    # spill
081     addl    RCX, #10    # int
084     imull   RCX, RCX    # int
087     sall    RBX, #1
089     sall    RCX, #1
08b     movl    RDX, R13    # spill
08e     addl    RDX, #8 # int
091     imull   RDX, RDX    # int
094     movl    RDI, R13    # spill
097     addl    RDI, #7 # int
09a     imull   RDI, RDI    # int
09d     sall    RDX, #1
09f     sall    RDI, #1
0a1     movl    RAX, R13    # spill
0a4     addl    RAX, #6 # int
0a7     imull   RAX, RAX    # int
0aa     movl    RSI, R13    # spill
0ad     addl    RSI, #4 # int
0b0     imull   RSI, RSI    # int
0b3     sall    RAX, #1
0b5     sall    RSI, #1
0b7     movl    R10, R13    # spill
0ba     addl    R10, #2 # int
0be     imull   R10, R10    # int
0c2     movl    R14, R13    # spill
0c5     incl    R14 # int
0c8     imull   R14, R14    # int
0cc     sall    R10, #1
0cf     sall    R14, #1
0d2     addl    R14, R11    # int
0d5     addl    R14, R10    # int
0d8     movl    R10, R13    # spill
0db     addl    R10, #3 # int
0df     imull   R10, R10    # int
0e3     movl    R11, R13    # spill
0e6     addl    R11, #5 # int
0ea     imull   R11, R11    # int
0ee     sall    R10, #1
0f1     addl    R10, R14    # int
0f4     addl    R10, RSI    # int
0f7     sall    R11, #1
0fa     addl    R11, R10    # int
0fd     addl    R11, RAX    # int
100     addl    R11, RDI    # int
103     addl    R11, RDX    # int
106     movl    R10, R13    # spill
109     addl    R10, #9 # int
10d     imull   R10, R10    # int
111     sall    R10, #1
114     addl    R10, R11    # int
117     addl    R10, RCX    # int
11a     addl    R10, RBX    # int
11d     addl    R10, R8 # int
120     addl    R9, R10 # int
123     addl    RBP, R9 # int
126     addl    RBP, [RSP + #32 (32-bit)]   # int
12a     addl    R13, #16    # int
12e     movl    R11, R13    # spill
131     imull   R11, R13    # int
135     sall    R11, #1
138     cmpl    R13, #999999985
13f     jl     B2   # loop end  P=1.000000 C=6554623.000000

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

对于2 * i * i版本:

05a   B3: # B2 B4 <- B1 B2  Loop: B3-B2 inner main of N18 Freq: 1e+006
05a     addl    RBX, R11    # int
05d     movl    [rsp + #32], RBX    # spill
061     movl    R11, R8 # spill
064     addl    R11, #15    # int
068     movl    [rsp + #36], R11    # spill
06d     movl    R11, R8 # spill
070     addl    R11, #14    # int
074     movl    R10, R9 # spill
077     addl    R10, #16    # int
07b     movdl   XMM2, R10   # spill
080     movl    RCX, R9 # spill
083     addl    RCX, #14    # int
086     movdl   XMM1, RCX   # spill
08a     movl    R10, R9 # spill
08d     addl    R10, #12    # int
091     movdl   XMM4, R10   # spill
096     movl    RCX, R9 # spill
099     addl    RCX, #10    # int
09c     movdl   XMM6, RCX   # spill
0a0     movl    RBX, R9 # spill
0a3     addl    RBX, #8 # int
0a6     movl    RCX, R9 # spill
0a9     addl    RCX, #6 # int
0ac     movl    RDX, R9 # spill
0af     addl    RDX, #4 # int
0b2     addl    R9, #2  # int
0b6     movl    R10, R14    # spill
0b9     addl    R10, #22    # int
0bd     movdl   XMM3, R10   # spill
0c2     movl    RDI, R14    # spill
0c5     addl    RDI, #20    # int
0c8     movl    RAX, R14    # spill
0cb     addl    RAX, #32    # int
0ce     movl    RSI, R14    # spill
0d1     addl    RSI, #18    # int
0d4     movl    R13, R14    # spill
0d7     addl    R13, #24    # int
0db     movl    R10, R14    # spill
0de     addl    R10, #26    # int
0e2     movl    [rsp + #40], R10    # spill
0e7     movl    RBP, R14    # spill
0ea     addl    RBP, #28    # int
0ed     imull   RBP, R11    # int
0f1     addl    R14, #30    # int
0f5     imull   R14, [RSP + #36 (32-bit)]   # int
0fb     movl    R10, R8 # spill
0fe     addl    R10, #11    # int
102     movdl   R11, XMM3   # spill
107     imull   R11, R10    # int
10b     movl    [rsp + #44], R11    # spill
110     movl    R10, R8 # spill
113     addl    R10, #10    # int
117     imull   RDI, R10    # int
11b     movl    R11, R8 # spill
11e     addl    R11, #8 # int
122     movdl   R10, XMM2   # spill
127     imull   R10, R11    # int
12b     movl    [rsp + #48], R10    # spill
130     movl    R10, R8 # spill
133     addl    R10, #7 # int
137     movdl   R11, XMM1   # spill
13c     imull   R11, R10    # int
140     movl    [rsp + #52], R11    # spill
145     movl    R11, R8 # spill
148     addl    R11, #6 # int
14c     movdl   R10, XMM4   # spill
151     imull   R10, R11    # int
155     movl    [rsp + #56], R10    # spill
15a     movl    R10, R8 # spill
15d     addl    R10, #5 # int
161     movdl   R11, XMM6   # spill
166     imull   R11, R10    # int
16a     movl    [rsp + #60], R11    # spill
16f     movl    R11, R8 # spill
172     addl    R11, #4 # int
176     imull   RBX, R11    # int
17a     movl    R11, R8 # spill
17d     addl    R11, #3 # int
181     imull   RCX, R11    # int
185     movl    R10, R8 # spill
188     addl    R10, #2 # int
18c     imull   RDX, R10    # int
190     movl    R11, R8 # spill
193     incl    R11 # int
196     imull   R9, R11 # int
19a     addl    R9, [RSP + #32 (32-bit)]    # int
19f     addl    R9, RDX # int
1a2     addl    R9, RCX # int
1a5     addl    R9, RBX # int
1a8     addl    R9, [RSP + #60 (32-bit)]    # int
1ad     addl    R9, [RSP + #56 (32-bit)]    # int
1b2     addl    R9, [RSP + #52 (32-bit)]    # int
1b7     addl    R9, [RSP + #48 (32-bit)]    # int
1bc     movl    R10, R8 # spill
1bf     addl    R10, #9 # int
1c3     imull   R10, RSI    # int
1c7     addl    R10, R9 # int
1ca     addl    R10, RDI    # int
1cd     addl    R10, [RSP + #44 (32-bit)]   # int
1d2     movl    R11, R8 # spill
1d5     addl    R11, #12    # int
1d9     imull   R13, R11    # int
1dd     addl    R13, R10    # int
1e0     movl    R10, R8 # spill
1e3     addl    R10, #13    # int
1e7     imull   R10, [RSP + #40 (32-bit)]   # int
1ed     addl    R10, R13    # int
1f0     addl    RBP, R10    # int
1f3     addl    R14, RBP    # int
1f6     movl    R10, R8 # spill
1f9     addl    R10, #16    # int
1fd     cmpl    R10, #999999985
204     jl     B2   # loop end  P=1.000000 C=7419903.000000

出于保存中间结果的需要,这里出现了更多的“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结果):

  vmovdqa ymm0, YMMWORD PTR .LC0[rip]vmovdqa ymm3, YMMWORD PTR .LC1[rip]xor eax, eaxvpxor xmm2, xmm2, xmm2
.L2:vpmulld ymm1, ymm0, ymm0inc eaxvpaddd ymm0, ymm0, ymm3vpslld ymm1, ymm1, 1vpaddd ymm2, ymm2, ymm1cmp eax, 125000000      ; 8 calculations per iterationjne .L2vmovdqa xmm0, xmm2vextracti128 xmm2, ymm2, 1vpaddd xmm2, xmm0, xmm2vpsrldq xmm0, xmm2, 8vpaddd xmm0, xmm2, xmm0vpsrldq xmm1, xmm0, 4vpaddd xmm0, xmm0, xmm1vmovd eax, xmm0vzeroupper

运行时间:

  • 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

想知道更多?描下面的二维码关注我


怎么加群?:

怎么免费加入知识星球:

免费资料入口:后台回复“666”

掘金小册支付通道开启,全场8折优惠。有需要购买电子版的《深入理解Kafka》的同学可以在公众号后台回复:【电子版】 或者 【Kafka】获取海报购买。

感谢阅读

朕已阅 

为什么 Java 中 2*(i*i) 比 2*i*i 更快?相关推荐

  1. 快手二面:Java 里的 for (;;) 与 while (true),哪个更快?

    转自:知乎 www.zhihu.com/question/52311366/answer/1300903 在 JDK8u 的 jdk 项目下做个很粗略的搜索: mymbp:/Users/me/work ...

  2. Java里的 for (;;) 与 while (true),哪个更快?

    在JDK8u的jdk项目下做个很粗略的搜索: 并没有差多少. 其次,for (;;) 在Java中的来源.个人看法是喜欢用这种写法的人,追根溯源是受到C语言里的写法的影响.这些人不一定是自己以前写C习 ...

  3. Java 里的 for (;;) 与 while (true),哪个更快?

    点击关注公众号,Java干货及时送达 在JDK8u的jdk项目下做个很粗略的搜索: mymbp:/Users/me/workspace/jdk8u/jdk/src $ egrep -nr " ...

  4. java如何造假数据_如何让数据“造假”更快更真?

    原标题:如何让数据"造假"更快更真? 看了前些天老祝的一个专门"造假"的函数,你用过吗?,觉得迭代计算用的比较少,开开关关有点麻烦,就想着怎么用公式来解决,顺便 ...

  5. java为何重复调用方法_通过反射调用Java中的getter:重复调用它的最快方法是什么(在性能和可伸缩性方面)?...

    小编典典 您可以使用MethodHandle.其Javadoc写道: 使用Lookup API中的工厂方法,可以将Core Reflection API对象表示的任何类成员转换为行为等效的方法句柄.例 ...

  6. java怎么截取后8位数字_java如何更快生成00000000~99999999的8位数字保存到文件中?...

    你的代码之所以会这么慢主要因为两个半半点: 1:你循环多少次就打开多少次文件. 2:你用了 String.format("%08d", atLong) 你应该先转换成String再 ...

  7. java中的values函数_巧用valueat函数(快逸免费版)

    在制作报表时,经常会遇到将数据库里一列数据按照条件取值后,分为多列显示的需求,例如:数据库中有一列名为type的数据,在报表中,第一列选择type为1的数据,第二列选择type为2的数据.由于受到扩展 ...

  8. Java中山脉的绘制---递归方法

    ** Java中山脉的绘制-递归方法 ** 一.山脉绘制的思路: 给定两个点A(x1,y1),B(x2,y2),递归不断取中点,同时给定一个范围[-range,range]和一个比率rate.每次取中 ...

  9. 如何在Java中编写正确的微基准测试?

    您如何用Java编写(并运行)正确的微基准测试? 我正在寻找一些代码示例和注释,以说明要考虑的各种问题. 示例:基准测试应测量时间/迭代或迭代/时间,为什么? 相关: 秒表基准测试是否可以接受? #1 ...

最新文章

  1. Python 包安装和 postgresql 的一些问题
  2. vim 之中 U 命令的浅析
  3. 《中国人工智能学会通讯》——10.10 结束语
  4. 蓝桥杯java第八届第八题--包子凑数
  5. EOJ_1094_寻找航海路线
  6. 自由职业的前半年,我是如何度过的?
  7. 前端依赖包管理-bower
  8. 基于微服务的软件架构模式
  9. 使用ZipCodeValidatorDomainType验证不同国家的邮编
  10. 06. Django基础:GET请求和POST请求
  11. 如何在WINDOWS XP下卸载打印机驱动程序
  12. Android:微信(二):解决问题
  13. M语言简单示例--网页数据抓取
  14. 计算机基础知识msoffice高级应用,2017计算机二级MS Office高级应用考试大纲
  15. ZYNQ OV7725和OV5640摄像头速度测试
  16. tapestry3常见问题
  17. Navigation网站收藏和导航平台
  18. 调试的时候没有在断点处停止的原因
  19. 如何离线下载网易云音乐
  20. ODOO开发教程之图表

热门文章

  1. Jenkins Pipeline 语法
  2. java取子串_Java中获取子字符串的几种方法示例
  3. 在 WebStorm 中误添加自定义的 HTML 属性,如何删除
  4. 【报错笔记】sturts2程序运行出现错误:Struts has detected an unhandled exception
  5. 静态内存、动态内存与堆栈
  6. 针对Selenium环境搭建的一些小解说
  7. Hackthissite realistic 6解密题后的记录
  8. 《“笨办法”学Python(第3版)》——习题3 数字和数学计算
  9. window下在同一台机器上安装多个版本jdk,修改环境变量不生效问题处理办法
  10. c++ STL平常练习-1