我有一个如下所示的Java循环:
public void testMethod() {
int[] nums = new int[10];
for (int i = 0; i < nums.length; i++) {
nums[i] = 0x42;
}
}
我得到的集会是:
0x00000001296ac845: cmp %r10d,%ebp
0x00000001296ac848: jae 0x00000001296ac8b4
0x00000001296ac84a: movl $0x42,0x10(%rbx,%rbp,4)
0x00000001296ac852: inc %ebp
0x00000001296ac854: cmp %r11d,%ebp
0x00000001296ac857: jl 0x00000001296ac845
0x00000001296ac859: mov %r10d,%r8d
0x00000001296ac85c: add $0xfffffffd,%r8d
0x00000001296ac860: mov $0x80000000,%r9d
0x00000001296ac866: cmp %r8d,%r10d
0x00000001296ac869: cmovl %r9d,%r8d
0x00000001296ac86d: cmp %r8d,%ebp
0x00000001296ac870: jge 0x00000001296ac88e
0x00000001296ac872: vmovq -0xda(%rip),%xmm0
0x00000001296ac87a: vpunpcklqdq %xmm0,%xmm0,%xmm0
0x00000001296ac87e: xchg %ax,%ax
0x00000001296ac880: vmovdqu %xmm0,0x10(%rbx,%rbp,4)
0x00000001296ac886: add $0x4,%ebp
0x00000001296ac889: cmp %r8d,%ebp
0x00000001296ac88c: jl 0x00000001296ac880
如果我的理解是正确的,那么第一个装配块就是nums[i] = 0x42;
。在第三个区块中,有vmovdqu
vmovdqu指令将值从整数向量移动到未对齐的内存位置。
但是,我仍然不能完全理解vmovdqu
在我的循环中所做的事情。
第三块汇编代码到底是做什么的?
此处提供了完整的代码:https://pastebin.com/cT5cJcMS
答案 0 :(得分:6)
优化器已选择对循环进行矢量化,每次&#34;迭代&#34;设置4个值。 (vmovdqu
之前的说明相当不透明,但可能会将0x42
展开到XMM0
的所有通道中。)&#34;未对齐&#34;变量是必要的,因为不保证数组在内存中是SIMD对齐的(毕竟,它存储int32
s,而不是int32x4
s。
答案 1 :(得分:3)
您的JIT编译器自动向量化循环,每个asm迭代存储4 int
个。
但它使得asm过于复杂并错过了很多优化。我想知道这可能只是JIT之前的第一阶段代码编译器决定完全优化?
您的代码没有返回nums
,因此它在创建后就被销毁了。内联后,您的功能应优化至无指令。或者作为一个独立的功能,应该只是一个ret
。分配内存然后让它被垃圾收集并不是优化器需要保留的可观察的副作用。
然后,如果new
成功,则nums.length
将为10
。所以代码可以像
# %rbx holds a pointer to the actual data storage for nums[]
vbroadcastss -0x????(%rip),%xmm0 # broadcast-load the 0x42 constant into xmm0
vmovdqu %xmm0, (%rbx) # nums[0..3] : 16 bytes
vmovdqu %xmm0, 16(%rbx) # nums[4..7] : 16 bytes
vmovq %xmm0, 32(%rbx) # nums[8..9] : 8 bytes
完全展开循环在这里最有意义;设置循环计数器等比一些商店需要更多的指令和代码大小。特别是当尺寸不是矢量宽度的倍数时,最后的部分矢量必须特别处理。
顺便说一句,如果你的大小是11而不是10,你可以做8 + 4字节存储,或者部分重叠的16字节存储,例如: 16字节vmovdqu
存储到(%rbx)
,16(%rbx)
和28(%rbx)
覆盖nums[7..11]
。在数组末尾结束的最终未对齐向量是手动向量化时的常用策略(或者在glibc的memcpy
的小缓冲区处理中),但即使是提前编译器也没有#39} ;似乎使用它。
其他明显错过的优化:
vmovq
加载+ vpunpcklqdq
进行广播。在AVX可用的情况下,vbroadcastss
是迄今为止从内存广播32位常量的最佳方式。一条指令不需要ALU uop。也许JIT编译器实际上并不了解新的AVX指令?
mov %r10d,%r8d
+ add $-3,%r8d
:这显然应该是lea -3(%r10), %r8d
。
不清楚%ebp
的起始值是什么;如果JVM在一个地方切片缓冲区,所以RBX不是数组的基础,那么EBMP在标量循环之前可能不是0? IDK为什么标量循环的循环绑定在寄存器中,而不是直接的。
将静态数据放在与代码相同的页面中是很奇怪的(-0xda(%rip)
仍在同一页面中)。没有大的惩罚,但这意味着需要在iTLB和dTLB中使用相同的页面,因此与使用单独的页面相比,您覆盖的代码+数据总量更少。但是,对于2M大页面来说并不是一件大事。共享的第二级TLB是受害者缓存(IIRC),因此填充它的iTLB未命中可能不会帮助vmovq
加载获得TLB命中。它可能会走第二页。
我不知道为什么即使是好的提前编译器如gcc和clang也会过于复杂化,因为对于具有未知对齐和长度的数组的循环。
void set42(int *nums, unsigned long int len) {
for (unsigned long int i=0 ; i<len ; i++ ) {
*nums++ = 0x42;
}
}
这就是我手工完成的,对于没有循环展开的128位向量(乐观地假设它不值得到达对齐边界,就像你的JIT一样,和clang和gcc8及更高版本一样):
# x86-64 System V calling convention: int*nums in RDI, len in RSI
set42:
cmp $4, %rsi
jb .Lsmall_count
lea -16(%rdi, %rsi,4), %rdx # pointer to end-16, the start of the final vector store
vbroadcastss constant(%rip), %xmm0
.p2align 4
.Lvector: # do {
vmovdqu %xmm0, (%rdi)
add $16, %rdi # nums += 4 elements
cmp %rdx, %rdi
jb .Lvector # while(nums < end-16);
# only reached for sizes >= 16 bytes so we can always store a full possibly-overlapping final vector
# for len = 16, this results in 2 stores to the same address, but that's cheaper than extra branches even if len=16 is common
vmovdqu %xmm0, (%rdx) # final potentially-overlapping vector
ret
.Lsmall_count:
test %rsi,%rsi
jz .Ldone
# some compilers will fully unroll this with a chain of branches
# maybe worth doing if small inputs are common
.Lscalar: # do {
movl 0x42, (%rdi)
add $4, %rdi # *num++ = 0x42;
dec %rsi
jnz # }while(--len);
# a more sophisticated cleanup strategy using SIMD is possible, e.g. 8-byte stores,
# but I haven't bothered.
.Ldone:
ret
请注意,对于len>=4
,顶部有一个直通分支,然后只有循环分支。总开销为1个宏融合cmp / jcc,1个广播负载和1个lea
。循环为3 uop,具有非索引寻址模式。
AFAIK,编制者不知道如何有效地使用可能重叠的最后一个向量。它在大多数时间里比标量清理要好得多。请注意,对于len = 4(16字节),我们执行相同的向量存储两次。但是对于len = 8(32字节),循环在第一次迭代后退出,因此我们仍然只进行2次总存储。即,除了1以外的矢量宽度的任何精确倍数,我们都不会重叠存储。对于len = 4和len = 8,以相同的方式分支对于分支预测实际上是很好的。
即使是好的C语言编译器也会让这个超级复杂,as you can see on the Godbolt compiler explorer。铿锵声的一些复杂性来自于展开更多; clang6.0展开了很多次。 (我选择了导致代码最简单的编译器版本和选项.gcc7.3和clang6.0为此发出了更大的函数。)
gcc7和更早版本的标量直到对齐边界,然后使用对齐的矢量存储。如果你指望指针经常未对齐,这可能会很好,但保存指令以使对齐的情况更便宜,通常是好的,并且未对齐存储的代价很低。
答案 2 :(得分:2)
编译器已展开循环以启用向量化。
// 10d holds the length of the array and ebp holds the loop index.
0x00000001296ac845: cmp %r10d,%ebp
// This branch is only taken when the loop index `i` is larger or equal to `nums.length`.
0x00000001296ac848: jae 0x00000001296ac8b4
// Performs a single iteration.
0x00000001296ac84a: movl $0x42,0x10(%rbx,%rbp,4)
// Increment the loop index.
0x00000001296ac852: inc %ebp
// r11d contains some constant. This is just to ensure that the number of any remaining iterations is multiple of 4.
0x00000001296ac854: cmp %r11d,%ebp
// This branch is NOT taken (falls through) only when either zero iterations are left of when the number of remaining iterations is a multiple of 4.
0x00000001296ac857: jl 0x00000001296ac845
// These instructions make sure that the loop index does not overflow.
0x00000001296ac859: mov %r10d,%r8d
0x00000001296ac85c: add $0xfffffffd,%r8d
0x00000001296ac860: mov $0x80000000,%r9d
0x00000001296ac866: cmp %r8d,%r10d
0x00000001296ac869: cmovl %r9d,%r8d
// The next two instructions check whether there are any remaining iterations.
0x00000001296ac86d: cmp %r8d,%ebp
0x00000001296ac870: jge 0x00000001296ac88e
// If we reach here, the number of remaining iterations must be a multiple of 4.
// Initialize xmm0 with 4 copies of 0x42.
0x00000001296ac872: vmovq -0xda(%rip),%xmm0
0x00000001296ac87a: vpunpcklqdq %xmm0,%xmm0,%xmm0
// This is a NOP just to align the loop on a 64-byte cache line boundary for performance.
0x00000001296ac87e: xchg %ax,%ax
// Vectorized 4 iterations of the loop.
0x00000001296ac880: vmovdqu %xmm0,0x10(%rbx,%rbp,4)
0x00000001296ac886: add $0x4,%ebp
0x00000001296ac889: cmp %r8d,%ebp
0x00000001296ac88c: jl 0x00000001296ac880
// All iterations have been executed at this point.