vmovdqu在这做什么?

Lit*_*ild 3 x86 assembly jvm-hotspot avx java-bytecode-asm

我有一个Java循环,如下所示:

public void testMethod() {
    int[] nums = new int[10];
    for (int i = 0; i < nums.length; i++) {
        nums[i] = 0x42;
    }
} 
Run Code Online (Sandbox Code Playgroud)

我得到的组件是这样的:

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  
Run Code Online (Sandbox Code Playgroud)

如果我的理解是正确的,那么第一个装配块就是那个nums[i] = 0x42;.在第三个区块中,有vmovdqu哪个

vmovdqu指令将值从整数向量移动到未对齐的内存位置.

但是,我仍然不完全理解vmovdqu我的循环环境中正在做什么.

第三块汇编代码到底是做什么的?

完整的代码可以在这里找到:https://pastebin.com/cT5cJcMS

Pet*_*des 8

您的 JIT 编译器自动矢量化您的循环,int每次 asm 迭代存储 4秒。

但它使 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
Run Code Online (Sandbox Code Playgroud)

在这里完全展开循环最有意义;设置循环计数器等需要比几个存储更多的指令和代码大小。特别是当大小不是向量宽度的倍数时,无论如何都必须特别处理最后一个部分向量。

顺便说一句,如果你的大小一直是11而不是10,你既可以做8个+ 4字节存储,或16字节存储的是部分重叠,例如16字节vmovdqu商店(%rbx)16(%rbx)以及28(%rbx)覆盖nums[7..11]。在手动矢量化(或在 glibc 的小缓冲区处理中memcpy)时,在数组末尾结束的最终未对齐向量是一种常见策略,但即使是提前编译器似乎也不使用它。


其他明显遗漏的优化:

  • vmovq加载 +vpunpcklqdq广播。有了 AVX,这vbroadcastss是迄今为止从内存中广播 32 位常量的最佳方式。一条不需要 ALU uop 的指令。也许 JIT 编译器实际上并不知道新的 AVX 指令?

  • mov %r10d,%r8d+ add $-3,%r8d:这显然应该是一个lea -3(%r10), %r8d.

目前尚不清楚 的起始值%ebp应该是多少;如果 JVM 正在某处切片缓冲区的块,因此 RBX 不是数组的基础,那么在标量循环之前 EBP 可能不是 0?IDK 为什么标量循环的循环边界在寄存器中,而不是立即数。

将静态数据与代码放在同一页(-0xda(%rip)仍然在同一页)很奇怪。没有太大的损失,但这意味着 iTLB 和 dTLB 中需要相同的页面,因此与使用单独的页面相比,您覆盖的总代码 + 数据更少。不过,对于 2M 大页面来说并不是什么大问题。共享的第二级 TLB 是受害者缓存 (IIRC),因此填充它的 iTLB 未命中可能不会帮助vmovq负载获得 TLB 命中。它可能会进行第二页遍历。


我不知道为什么即使是像 gcc 和 clang 这样的优秀的提前 C 编译器,为了在具有未知对齐方式和长度的数组上进行循环,也会使这个问题变得如此复杂。

# %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
Run Code Online (Sandbox Code Playgroud)

这是我手工做的,对于没有循环展开的 128 位向量(并且乐观地假设它不值得达到对齐边界,比如你的 JIT,像 clang 和 gcc8 及更高版本):

void set42(int *nums, unsigned long int len) {
    for (unsigned long int i=0 ; i<len ; i++ ) {
        *nums++ = 0x42;
    }
}
Run Code Online (Sandbox Code Playgroud)

请注意 for len>=4,顶部有一个 fall-through 分支,然后只有循环分支。总开销为 1 个宏融合 cmp/jcc、1 个广播负载和 1 个lea. 循环是 3 uop,采用非索引寻址模式。

AFAIK,编译器不知道如何有效地使用可能重叠的最后一个向量。大多数情况下,它比标量清理要好得多。请注意,对于 len=4(16 字节),我们执行相同的向量存储两次。但是对于 len=8(32 字节),循环在第一次迭代后退出,所以我们仍然只进行 2 次存储。即对于除 1 以外的向量宽度的任何精确倍数,我们不进行重叠存储。对于 len=4 和 len=8 以相同的方式进行分支实际上对分支预测很好。


正如您在 Godbolt 编译器资源管理器中看到的那样,即使是良好的提前 C 编译器也会使这变得非常复杂。clang 的一些复杂性来自于展开更多;clang6.0 展开了大量的时间。(我选择了导致代码最不复杂的编译器版本和选项。gcc7.3 和 clang6.0 为此发出了更大的函数。)

gcc7 和更早版本使用标量直到对齐边界,然后使用对齐的向量存储。如果您希望指针经常未对齐,这可能很好,但是在通常情况下保存指令以使对齐的情况更便宜是好的,并且对未对齐存储的惩罚很低。


Sne*_*tel 6

优化器选择对循环进行矢量化,每次"迭代"设置4个值.(前面的说明vmovdqu是相当不透明的,但可能是它会溅0x42入所有通道中XMM0.)"未对齐"变体是必要的,因为数组不保证在内存中SIMD对齐(毕竟,它存储int32s,而不是int32x4s) .

  • 我不会这么称呼它.身体不会多次复制.矢量化是一个更好的词. (2认同)
  • @LittleChild:向量化是循环展开+与SIMD并行进行多次迭代.如果你展开更多并且每个asm循环迭代做多个向量,你通常只称它为"展开". (2认同)