【问题标题】:Why doesn't the JVM emit prefetch instructions on Windows x86为什么 JVM 在 Windows x86 上不发出预取指令
【发布时间】:2017-11-05 10:00:15
【问题描述】:

如标题所述,为什么 OpenJDK JVM 在 Windows x86 上不发出预取指令?见 OpenJDK Mercurial @http://hg.openjdk.java.net/jdk8u/jdk8u/hotspot/file/c49dcaf78a65/src/os_cpu/windows_x86/vm/prefetch_windows_x86.inline.hpp

inline void Prefetch::read (void *loc, intx interval) {}
inline void Prefetch::write(void *loc, intx interval) {}

没有 cmets,除了源代码,我没有找到其他资源。我问是因为它适用于 Linux x86,请参阅http://hg.openjdk.java.net/jdk8u/jdk8u/hotspot/file/c49dcaf78a65/src/os_cpu/linux_x86/vm/prefetch_linux_x86.inline.hpp

inline void Prefetch::read (void *loc, intx interval) {
#ifdef AMD64
  __asm__ ("prefetcht0 (%0,%1,1)" : : "r" (loc), "r" (interval));
#endif // AMD64
}

inline void Prefetch::write(void *loc, intx interval) {
#ifdef AMD64

  // Do not use the 3dnow prefetchw instruction.  It isn't supported on em64t.
  //  __asm__ ("prefetchw (%0,%1,1)" : : "r" (loc), "r" (interval));
  __asm__ ("prefetcht0 (%0,%1,1)" : : "r" (loc), "r" (interval));

#endif // AMD64
}

【问题讨论】:

  • solaris x86_64 也有预取:vm/solaris_x86_64.il github.com/openjdk-mirror/jdk7u-hotspot/blob/…;但所有列出的预取都不是用于发出预取,它们是供 JVM 热点机器代码本身使用的预取。在生成的(JITted)代码中发出预取是所有操作系统的 x86 代码:github.com/openjdk-mirror/jdk7u-hotspot/blob/…LIR_Assembler::prefetchr/LIR_Assembler::prefetchw
  • 谢谢,这至少解释了一些事情。也许将此添加为评论,我会接受。我仍在寻找 JVM 决定插入预取指令的部分。

标签: java assembly x86 jvm jvm-hotspot


【解决方案1】:

您引用的文件都有 asm 代码片段(inline assembler),一些 C/C++ 软件在其自己的代码中使用该代码片段(如apangin, the JVM expertpointed,主要在 GC 代码中)。实际上存在区别:x86_64 热点的LinuxSolarisBSD 变体在热点中具有预取,而 Windows 将它们禁用/未实现,这部分是奇怪的,部分是无法解释的,它也可能使 JVM 位(一些百分比;在没有硬件预取的平台上更多)在 Windows 上较慢,但仍无助于销售更多的 Sun/Oracle 的 solaris/solaris 付费支持合同。 Ross also guessed MS C++ 编译器可能不支持内联 asm 语法,但 _mm_prefetch 应该(谁会打开 JDK 错误来添加它to the file?)。

JVM 热点是 JIT,JIT 处理的代码由 JIT 作为字节发出(生成)(虽然 JIT 可以将代码从其自己的函数复制到生成的代码或发出对支持函数的调用,但发出预取作为热点中的字节)。我们如何才能找到它是如何发出的?简单的在线方法是找到一些 jdk8u 的在线可搜索副本(或者在 cross-reference like metager 中更好),例如在 github 上:https://github.com/JetBrains/jdk8u_hotspot 并搜索 prefetchprefetch emitprefetchrlir_prefetchr。有一些相关的结果:

在 JVM 的 c1 compiler / LIR jdk8u_hotspot/src/cpu/x86/vm/assembler_x86.cpp 中发出的实际字节数:

void Assembler::prefetch_prefix(Address src) {
  prefix(src);
  emit_int8(0x0F);
}

void Assembler::prefetchnta(Address src) {
  NOT_LP64(assert(VM_Version::supports_sse(), "must support"));
  InstructionMark im(this);
  prefetch_prefix(src);
  emit_int8(0x18);
  emit_operand(rax, src); // 0, src
}

void Assembler::prefetchr(Address src) {
  assert(VM_Version::supports_3dnow_prefetch(), "must support");
  InstructionMark im(this);
  prefetch_prefix(src);
  emit_int8(0x0D);
  emit_operand(rax, src); // 0, src
}

void Assembler::prefetcht0(Address src) {
  NOT_LP64(assert(VM_Version::supports_sse(), "must support"));
  InstructionMark im(this);
  prefetch_prefix(src);
  emit_int8(0x18);
  emit_operand(rcx, src); // 1, src
}

void Assembler::prefetcht1(Address src) {
  NOT_LP64(assert(VM_Version::supports_sse(), "must support"));
  InstructionMark im(this);
  prefetch_prefix(src);
  emit_int8(0x18);
  emit_operand(rdx, src); // 2, src
}

void Assembler::prefetcht2(Address src) {
  NOT_LP64(assert(VM_Version::supports_sse(), "must support"));
  InstructionMark im(this);
  prefetch_prefix(src);
  emit_int8(0x18);
  emit_operand(rbx, src); // 3, src
}

void Assembler::prefetchw(Address src) {
  assert(VM_Version::supports_3dnow_prefetch(), "must support");
  InstructionMark im(this);
  prefetch_prefix(src);
  emit_int8(0x0D);
  emit_operand(rcx, src); // 1, src
}

在 c1 LIR 中的使用:src/share/vm/c1/c1_LIRAssembler.cpp

void LIR_Assembler::emit_op1(LIR_Op1* op) {
  switch (op->code()) { 
...
    case lir_prefetchr:
      prefetchr(op->in_opr());
      break;

    case lir_prefetchw:
      prefetchw(op->in_opr());
      break;

现在我们知道the opcode lir_prefetchr and can search for itOpenGrok xreflir_prefetchw,在src/share/vm/c1/c1_LIR.cpp 中找到唯一的例子

void LIR_List::prefetch(LIR_Address* addr, bool is_store) {
  append(new LIR_Op1(
            is_store ? lir_prefetchw : lir_prefetchr,
            LIR_OprFact::address(addr)));
}

还有其他地方定义了预取指令(对于C2,如noted by apangin),the src/cpu/x86/vm/x86_64.ad

// Prefetch instructions. ...
instruct prefetchr( memory mem ) %{
  predicate(ReadPrefetchInstr==3);
  match(PrefetchRead mem);
  ins_cost(125);

  format %{ "PREFETCHR $mem\t# Prefetch into level 1 cache" %}
  ins_encode %{
    __ prefetchr($mem$$Address);
  %}
  ins_pipe(ialu_mem);
%}

instruct prefetchrNTA( memory mem ) %{
  predicate(ReadPrefetchInstr==0);
  match(PrefetchRead mem);
  ins_cost(125);

  format %{ "PREFETCHNTA $mem\t# Prefetch into non-temporal cache for read" %}
  ins_encode %{
    __ prefetchnta($mem$$Address);
  %}
  ins_pipe(ialu_mem);
%}

instruct prefetchrT0( memory mem ) %{
  predicate(ReadPrefetchInstr==1);
  match(PrefetchRead mem);
  ins_cost(125);

  format %{ "PREFETCHT0 $mem\t# prefetch into L1 and L2 caches for read" %}
  ins_encode %{
    __ prefetcht0($mem$$Address);
  %}
  ins_pipe(ialu_mem);
%}

instruct prefetchrT2( memory mem ) %{
  predicate(ReadPrefetchInstr==2);
  match(PrefetchRead mem);
  ins_cost(125);

  format %{ "PREFETCHT2 $mem\t# prefetch into L2 caches for read" %}
  ins_encode %{
    __ prefetcht2($mem$$Address);
  %}
  ins_pipe(ialu_mem);
%}

instruct prefetchwNTA( memory mem ) %{
  match(PrefetchWrite mem);
  ins_cost(125);

  format %{ "PREFETCHNTA $mem\t# Prefetch to non-temporal cache for write" %}
  ins_encode %{
    __ prefetchnta($mem$$Address);
  %}
  ins_pipe(ialu_mem);
%}

// Prefetch instructions for allocation.

instruct prefetchAlloc( memory mem ) %{
  predicate(AllocatePrefetchInstr==3);
  match(PrefetchAllocation mem);
  ins_cost(125);

  format %{ "PREFETCHW $mem\t# Prefetch allocation into level 1 cache and mark modified" %}
  ins_encode %{
    __ prefetchw($mem$$Address);
  %}
  ins_pipe(ialu_mem);
%}

instruct prefetchAllocNTA( memory mem ) %{
  predicate(AllocatePrefetchInstr==0);
  match(PrefetchAllocation mem);
  ins_cost(125);

  format %{ "PREFETCHNTA $mem\t# Prefetch allocation to non-temporal cache for write" %}
  ins_encode %{
    __ prefetchnta($mem$$Address);
  %}
  ins_pipe(ialu_mem);
%}

instruct prefetchAllocT0( memory mem ) %{
  predicate(AllocatePrefetchInstr==1);
  match(PrefetchAllocation mem);
  ins_cost(125);

  format %{ "PREFETCHT0 $mem\t# Prefetch allocation to level 1 and 2 caches for write" %}
  ins_encode %{
    __ prefetcht0($mem$$Address);
  %}
  ins_pipe(ialu_mem);
%}

instruct prefetchAllocT2( memory mem ) %{
  predicate(AllocatePrefetchInstr==2);
  match(PrefetchAllocation mem);
  ins_cost(125);

  format %{ "PREFETCHT2 $mem\t# Prefetch allocation to level 2 cache for write" %}
  ins_encode %{
    __ prefetcht2($mem$$Address);
  %}
  ins_pipe(ialu_mem);
%}

【讨论】:

  • 这里是 JVM 实际决定是否预取的更有趣的部分之一github.com/JetBrains/jdk8u_hotspot/blob/…
  • 我实际上正在撰写一篇科学论文,其中包含诸如“JVM JIT 进行预取”之类的句子。由于没有关于 JVM 内部的真实论文,我只需要深入挖掘以找到证据,即使它是常识。学术界只是不这样工作:)
  • naze,我找不到 PrefetchAllocationNode 是如何实现到真正的操作码的,它上面有一些奇怪的 ABIO 标记。可能您需要在本地编译 JVM/JDK 以生成要生成的所有文件,然后搜索完整代码(可能使用一些 C++ 交叉引用工具;但请注意非 C++ 文件,如 asm 和 ad 未搜索通过交叉引用,仅限grep)。
  • 经过几个小时的搜索,我仍然不知道在哪里寻找做或不做的决​​定,例如数组访问的预取。我根本不了解 OpenJDK 的结构。不过感谢您的帮助。如果我发现任何东西会添加评论
  • 引用 C1 LIR 并不太有用,因为热方法通常使用 C2 编译。 x86_64 architecture definition 文件中列出了在 JITted 代码中发出预取指令的实际规则。
【解决方案2】:

正如JDK-4453409 所指出的,在 JDK 1.4 的 HotSpot JVM 中实现了预取以加速 GC。那是 15 多年前的事了,现在没有人记得为什么它没有在 Windows 上实现。我的猜测是 Visual Studio(一直用于在 Windows 上构建 HotSpot)在这些时候基本上不理解预取指令。看起来是个需要改进的地方。

无论如何,您所询问的代码在 JVM Garbage Collector 内部使用。这不是 JIT 生成的。 C2 JIT代码生成器规则在架构定义文件x86_64.ad中,有rulesPrefetchReadPrefetchWritePrefetchAllocation节点翻译成对应的x64指令。

一个有趣的事实是PrefetchReadPrefetchWrite 节点不是在代码中的任何地方创建的。它们的存在只是为了支持 Unsafe.prefetchX 内在函数,但是,它们在 JDK 9 中是 removed

JIT 生成预取指令的唯一情况是PrefetchAllocation 节点。您可以通过-XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly 验证PREFETCHNTA 确实是在对象分配之后生成的,在 Linux 和 Windows 上都

class Test {
    public static void main(String[] args) {
        byte[] b = new byte[0];
        for (;;) {
            b = Arrays.copyOf(b, b.length + 1);
        }
    }
}

java.exe -XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly Test

# {method} {0x00000000176124e0} 'main' '([Ljava/lang/String;)V' in 'Test'
  ...
  0x000000000340e512: cmp    $0x100000,%r11d
  0x000000000340e519: ja     0x000000000340e60f
  0x000000000340e51f: movslq 0x24(%rsp),%r10
  0x000000000340e524: add    $0x1,%r10
  0x000000000340e528: add    $0x17,%r10
  0x000000000340e52c: mov    %r10,%r8
  0x000000000340e52f: and    $0xfffffffffffffff8,%r8
  0x000000000340e533: cmp    $0x100000,%r11d
  0x000000000340e53a: ja     0x000000000340e496
  0x000000000340e540: mov    0x60(%r15),%rbp
  0x000000000340e544: mov    %rbp,%r9
  0x000000000340e547: add    %r8,%r9
  0x000000000340e54a: cmp    0x70(%r15),%r9
  0x000000000340e54e: jae    0x000000000340e496
  0x000000000340e554: mov    %r9,0x60(%r15)
  0x000000000340e558: prefetchnta 0xc0(%r9)
  0x000000000340e560: movq   $0x1,0x0(%rbp)
  0x000000000340e568: prefetchnta 0x100(%r9)
  0x000000000340e570: movl   $0x200000f5,0x8(%rbp)  ;   {metadata({type array byte})}
  0x000000000340e577: mov    %r11d,0xc(%rbp)
  0x000000000340e57b: prefetchnta 0x140(%r9)
  0x000000000340e583: prefetchnta 0x180(%r9)    ;*newarray
                                                ; - java.util.Arrays::copyOf@1 (line 3236)
                                                ; - Test::main@9 (line 9)

【讨论】:

  • +1 用于发现预取仅在分配上下文中使用。我猜想在迭代现有数组时也会进行预取。看来我的假设是错误的。感谢您的澄清
  • @naze,遍历数组会有预取;但它不是软件预取,而是硬件预取。您可以关闭它并测量它对 Intel 的影响:software.intel.com/en-us/articles/…(每个内核都带有wrmsr -p N 0x1a4); “0x1A0 位 9 和 19 用于旧处理器型号中” - stackoverflow.com/a/36339469。 Intel 的硬件预取是激进的,但仅限于 4KB 页面:如果它们捕获到两个内存访问 A 和 B,ptrdiff 为 N=B-A,并且 B+N 在相同的 4 KB 中,它们会进行预取。
  • @osgx 我已经知道硬件预取,我只是想知道 jvm 中的软件预取。还是谢谢
  • @naze:在具有良好硬件预取器的现代 CPU 上,使用软件预取进行顺序访问通常没有任何好处。 IvyBridge 及更高版本甚至可以跨 4k 页面边界预取。 (来源:这个答案的底部:stackoverflow.com/a/20758769/224132)。预取指令需要时间来运行,因此在实际上没有内存瓶颈的情况下,它们可以减慢您的代码速度。 (或者在英特尔 IvyBridge 上,根据 Agner Fog 的表,预取指令可能具有非常糟糕的吞吐量,例如每 43 个周期一个,并且本身就是一个瓶颈。)
猜你喜欢
  • 1970-01-01
  • 2014-05-06
  • 2023-03-14
  • 2015-01-05
  • 1970-01-01
  • 2014-01-08
  • 1970-01-01
  • 2017-03-04
  • 2019-06-20
相关资源
最近更新 更多