字节码的顺序略有不同。
2 * (i * i):
iconst_2 iload0 iload0 imul imul iadd
vs
2 * i * i:
iconst_2 iload0 imul iload0 imul iadd
乍看之下,这没有什么区别;如果有的话,第二个版本更理想,因为它减少了一个插槽。
因此,我们需要更深入地研究较低级别(JIT)1。
请记住,JIT倾向于非常积极地展开小循环。的确,我们发现该
2 * (i * i)案例的展开速度是16倍:
030 B2: # B2 B3 <- B1 B2 Loop: B2-B2 inner main of N18 Freq: 1e+006030 addl R11, RBP # int033 movl RBP, R13 # spill036 addl RBP, #14 # int039 imull RBP, RBP # int03c movl R9, R13 # spill03f addl R9, #13 # int043 imull R9, R9 # int047 sall RBP, #1049 sall R9, #104c movl R8, R13 # spill04f addl R8, #15 # int053 movl R10, R8 # spill056 movdl XMM1, R8 # spill05b imull R10, R8 # int05f movl R8, R13 # spill062 addl R8, #12 # int066 imull R8, R8 # int06a sall R10, #106d movl [rsp + #32], R10 # spill072 sall R8, #1075 movl RBX, R13 # spill078 addl RBX, #11 # int07b imull RBX, RBX # int07e movl RCX, R13 # spill081 addl RCX, #10 # int084 imull RCX, RCX # int087 sall RBX, #1089 sall RCX, #108b movl RDX, R13 # spill08e addl RDX, #8 # int091 imull RDX, RDX # int094 movl RDI, R13 # spill097 addl RDI, #7 # int09a imull RDI, RDI # int09d sall RDX, #109f sall RDI, #10a1 movl RAX, R13 # spill0a4 addl RAX, #6 # int0a7 imull RAX, RAX # int0aa movl RSI, R13 # spill0ad addl RSI, #4 # int0b0 imull RSI, RSI # int0b3 sall RAX, #10b5 sall RSI, #10b7 movl R10, R13 # spill0ba addl R10, #2 # int0be imull R10, R10 # int0c2 movl R14, R13 # spill0c5 incl R14 # int0c8 imull R14, R14 # int0cc sall R10, #10cf sall R14, #10d2 addl R14, R11 # int0d5 addl R14, R10 # int0d8 movl R10, R13 # spill0db addl R10, #3 # int0df imull R10, R10 # int0e3 movl R11, R13 # spill0e6 addl R11, #5 # int0ea imull R11, R11 # int0ee sall R10, #10f1 addl R10, R14 # int0f4 addl R10, RSI # int0f7 sall R11, #10fa addl R11, R10 # int0fd addl R11, RAX # int100 addl R11, RDI # int103 addl R11, RDX # int106 movl R10, R13 # spill109 addl R10, #9 # int10d imull R10, R10 # int111 sall R10, #1114 addl R10, R11 # int117 addl R10, RCX # int11a addl R10, RBX # int11d addl R10, R8 # int120 addl R9, R10 # int123 addl RBP, R9 # int126 addl RBP, [RSP + #32 (32-bit)] # int12a addl R13, #16 # int12e movl R11, R13 # spill131 imull R11, R13 # int135 sall R11, #1138 cmpl R13, #99999998513f jl B2 # loop end P=1.000000 C=6554623.000000
我们看到有1个寄存器被“堆放”到堆栈中。
对于
2 * i * i版本:
05a B3: # B2 B4 <- B1 B2 Loop: B3-B2 inner main of N18 Freq: 1e+00605a addl RBX, R11 # int05d movl [rsp + #32], RBX # spill061 movl R11, R8 # spill064 addl R11, #15 # int068 movl [rsp + #36], R11 # spill06d movl R11, R8 # spill070 addl R11, #14 # int074 movl R10, R9 # spill077 addl R10, #16 # int07b movdl XMM2, R10 # spill080 movl RCX, R9 # spill083 addl RCX, #14 # int086 movdl XMM1, RCX # spill08a movl R10, R9 # spill08d addl R10, #12 # int091 movdl XMM4, R10 # spill096 movl RCX, R9 # spill099 addl RCX, #10 # int09c movdl XMM6, RCX # spill0a0 movl RBX, R9 # spill0a3 addl RBX, #8 # int0a6 movl RCX, R9 # spill0a9 addl RCX, #6 # int0ac movl RDX, R9 # spill0af addl RDX, #4 # int0b2 addl R9, #2 # int0b6 movl R10, R14 # spill0b9 addl R10, #22 # int0bd movdl XMM3, R10 # spill0c2 movl RDI, R14 # spill0c5 addl RDI, #20 # int0c8 movl RAX, R14 # spill0cb addl RAX, #32 # int0ce movl RSI, R14 # spill0d1 addl RSI, #18 # int0d4 movl R13, R14 # spill0d7 addl R13, #24 # int0db movl R10, R14 # spill0de addl R10, #26 # int0e2 movl [rsp + #40], R10 # spill0e7 movl RBP, R14 # spill0ea addl RBP, #28 # int0ed imull RBP, R11 # int0f1 addl R14, #30 # int0f5 imull R14, [RSP + #36 (32-bit)] # int0fb movl R10, R8 # spill0fe addl R10, #11 # int102 movdl R11, XMM3 # spill107 imull R11, R10 # int10b movl [rsp + #44], R11 # spill110 movl R10, R8 # spill113 addl R10, #10 # int117 imull RDI, R10 # int11b movl R11, R8 # spill11e addl R11, #8 # int122 movdl R10, XMM2 # spill127 imull R10, R11 # int12b movl [rsp + #48], R10 # spill130 movl R10, R8 # spill133 addl R10, #7 # int137 movdl R11, XMM1 # spill13c imull R11, R10 # int140 movl [rsp + #52], R11 # spill145 movl R11, R8 # spill148 addl R11, #6 # int14c movdl R10, XMM4 # spill151 imull R10, R11 # int155 movl [rsp + #56], R10 # spill15a movl R10, R8 # spill15d addl R10, #5 # int161 movdl R11, XMM6 # spill166 imull R11, R10 # int16a movl [rsp + #60], R11 # spill16f movl R11, R8 # spill172 addl R11, #4 # int176 imull RBX, R11 # int17a movl R11, R8 # spill17d addl R11, #3 # int181 imull RCX, R11 # int185 movl R10, R8 # spill188 addl R10, #2 # int18c imull RDX, R10 # int190 movl R11, R8 # spill193 incl R11 # int196 imull R9, R11 # int19a addl R9, [RSP + #32 (32-bit)] # int19f addl R9, RDX # int1a2 addl R9, RCX # int1a5 addl R9, RBX # int1a8 addl R9, [RSP + #60 (32-bit)] # int1ad addl R9, [RSP + #56 (32-bit)] # int1b2 addl R9, [RSP + #52 (32-bit)] # int1b7 addl R9, [RSP + #48 (32-bit)] # int1bc movl R10, R8 # spill1bf addl R10, #9 # int1c3 imull R10, RSI # int1c7 addl R10, R9 # int1ca addl R10, RDI # int1cd addl R10, [RSP + #44 (32-bit)] # int1d2 movl R11, R8 # spill1d5 addl R11, #12 # int1d9 imull R13, R11 # int1dd addl R13, R10 # int1e0 movl R10, R8 # spill1e3 addl R10, #13 # int1e7 imull R10, [RSP + #40 (32-bit)] # int1ed addl R10, R13 # int1f0 addl RBP, R10 # int1f3 addl R14, RBP # int1f6 movl R10, R8 # spill1f9 addl R10, #16 # int1fd cmpl R10, #999999985204 jl B2 # loop end P=1.000000 C=7419903.000000
在这里
[RSP + ...],由于需要保留更多中间结果,因此观察到了更多的“溢出”和对堆栈的更多访问。
因此,问题的答案很简单:
2 * (i * i)比
2 * i * i第一种情况要快,因为JIT会生成更多的最佳汇编代码。
但是,显然第一版和第二版都不好。由于任何x86-64 CPU至少都支持SSE2,因此循环可以真正受益于向量化。
因此,这是优化程序的问题;通常情况下,它展开得过于猛烈,并在脚上开枪射击,而同时又错失了其他各种机会。
实际上,现代的x86-64
CPU将指令进一步细分为微操作(µop),并具有寄存器重命名,µop缓存和循环缓冲区等功能,与简单展开以达到最佳性能相比,循环优化需要更多的技巧。根据Agner
Fog的优化指南:
如果平均指令长度大于4个字节,则由µop缓存引起的性能提升会非常可观。可以考虑以下优化µop缓存使用的方法:
- 确保关键循环足够小以适合µop缓存。
- 将最关键的循环条目和功能条目对齐32。
- 避免不必要的循环展开。
- 避免使用具有额外加载时间的说明
。。。
关于这些加载时间-
即使最快的L1D命中也要花费4个周期,一个额外的寄存器和µop,所以是的,即使是对存储器的几次访问也会损害紧密循环中的性能。
但是回到矢量化的机会-
要了解它有多快,我们可以使用GCC编译类似的C应用程序,然后直接对其进行矢量化(显示为AVX2,SSE2相似)2:
vmovdqa ymm0, YMMWORD PTR .LC0[rip] vmovdqa ymm3, YMMWORD PTR .LC1[rip] xor eax, eax vpxor xmm2, xmm2, xmm2.L2: vpmulld ymm1, ymm0, ymm0 inc eax vpaddd ymm0, ymm0, ymm3 vpslld ymm1, ymm1, 1 vpaddd ymm2, ymm2, ymm1 cmp eax, 125000000 ; 8 calculations per iteration jne .L2 vmovdqa xmm0, xmm2 vextracti128 xmm2, ymm2, 1 vpaddd xmm2, xmm0, xmm2 vpsrldq xmm0, xmm2, 8 vpaddd xmm0, xmm2, xmm0 vpsrldq xmm1, xmm0, 4 vpaddd xmm0, xmm0, xmm1 vmovd eax, xmm0 vzeroupper
运行时间:
- SSE:0.24 s,或快2倍。
- AVX:0.15秒,或3倍快。
- AVX2:0.08 s,或快5倍。
1
要获取JIT生成的程序集输出,请获取调试JVM并运行
-XX:+PrintOptoAssembly
2 C版本使用
-fwrapv标志进行编译,这使GCC可以将带符号整数溢出视为二进制补码。



