方法分派的黑魔法(Java)

这里有你想知道的关于底层性能优化工作的黑魔法。

前言

像Java这类的语言为了模块化和复用,都提供了继承/多态的特性。
但是,程序员,代价是什么呢?
虚拟调用(virtual call)没有是硬件支持的,只能由JVM模拟这种行为,在许多情况下,方法分派(method dispatch)的性能并不重要,低级别的性能问题并不是真正的问题。
但是,在某些情况下,方法分派的性能很重要,你需要了解分派的工作原理,JVM做了哪些优化,以及如何在代码中模拟的类似行为。
例如,在字符串压缩的开发过程中,我们遇到了为给定字符串选择编码器的问题。我们创建一个可维护的Coder接口,做了一些实现,通过它分派虚拟调用,但在基准测试中遇到了一些性能问题。这需要优化,经过几次实验后,就有了这篇文章,供大家参考。
这篇文章也涉及到虚拟调用的内联,这是优化过程的自然产物。

作为优秀的传统,我们将采用面向基准测试和通用的底层性能工程的方式,即使本文本身是针对平台开发者,一般的开发者仍然可以学习一些技巧。按照惯例,如果你还没有了解JMH或没有查看过JMH例子,那么我建议你先阅读本文的其余部分,以获得最佳体验。

这篇文章还假设你对Java,Java字节码,编译器,JVM和x86指令集有很好的理解。许多读者抱怨我的文章缺乏完整性,因为我省略了如何得出特定结论的细节。这次我决定使用大量的注释来阐述如何解读编译器的输出,如何阅读生成的指令集等,如果你没有时间,可以忽略。

1.问题

我们将问题形式化如下。假设我们有一个类Data:

public class Data {
  byte[] data;
}

我们希望根据数据做不同的事情。假设我们有N个版本的代码,每个版本都提供了某种类型的data。例如,在字符串压缩中,data可以是1字节编码的数组,也可以是2字节编码的数组。我们需要为这些数据提供某种实现; 换句话说,解码它或者做其他的工作。例如,假设我们有一个“Coder”的抽象,它可以做以下操作:

public class Coder1 {
  int work(byte[] data) { return omNomNom(data); }
}
public class Coder2 {
  int work(byte[] data) { return cIsForCookie(data); }
}

那么问题是,实现不同的Coder并对其进行分派的最佳方法是什么?

2.实验配置

2.1 硬件

虽然各个平台之间确实存在差异,但事后看来,大多数行为是平台无关的,并且是在高级优化器中完成的。因此,我们简化了实验,并在一台1x4x2 i7-4790K 4.0 Ghz, Linux x86_64, JDK 8u40 EA的机器上运行测试。欢迎大家在各自的平台上重现结果。

2.2要测试的demo

当然,你可以直接将Coder的实现内联,但这值得商榷,特别是如果有多个Coder实现。因此,我们考虑这些方法来实现Coder:

  1. 静态:使用静态方法进行Coder的静态实现。
  2. Dynamic_Interface:创建一个合适的Coder接口,并提供实现。
  3. Dynamic_Abstract:与上面相同,但是创建Coder的抽象超类,并提供实现。

这三种实现都需要对Data进行编码。我们可以提出四种编码方案:

  1. ID_Switch:存储字节ID,通过switch选择编码器
  2. ID_IfElse:存储字节ID,通过if-else选择编码器
  3. Bool_IfElse:存储布尔值,通过if-else选择编码器。(仅适用于N = 2)
  4. Ref:存储Coder的引用,并进行虚拟调用。

还有其他编码方式,例如存储java.lang.reflect.Method或者java.lang.invoke.MethodHandle,先不关注,这都与Ref有些相似,并且还需要一些高级编译器魔法才能高效工作,我们将在未来介绍这些内容。

编码方案的选择还必须考虑空间占用,Data对象中的额外字段会增加实例大小。但是,由于Java对象通常以8个字节对齐,因此对象中存在相当大的“对齐阴影”,可以在不增加实例大小的情况下填充字段。隐藏boolean/ byte字段可能比整个引用字段更容易。JOL可用于查看字段/对象布局。

结合这些思考和可选方案,我们已经有很多方法变种了。并非所有的这些都是必需的,但剖析它们有助于理解JVM如何应对这样的代码。

2.3 基准测试

源代码请点击这里。由于我们面对的是VM处理细节的场景,让我解释一下基准测试的一些事项。
这是单个基准测试的案例:

public class Test {
    @Param("100")
    private int count;

    private Data[] datas;

    @Setup
    public void setup() {
        datas = new Data[count];

        Random r = new Random();
        for (int c = 0; c < count; c++) {
            byte[] contents = new byte[10];
            r.nextBytes(contents);
            datas[c] = new Data(r.nextInt(2), contents);
        }
    }

    @Benchmark
    public void dynamic_Interface_Ref() {
        Data[] l = datas;
        int c = count;
        for (int i = 0; i < c; i++) {
            l[i].do_Dynamic_Interface_Ref();
        }
    }
}

public class Coder0 implements Coder {
    public int work(byte[] data) {
        return data.length; // something light-weight
    }
}

public class Coder1 implements Coder {
    public int work(byte[] data) {
        return data.length; // something light-weight
    }
}

public class Data {
    private static final Coder0 coder0 = new Coder0();
    private static final Coder1 coder1 = new Coder1();
    private final Coder coder;
    private final byte id;
    private final byte[] data;

    public Data(byte id, byte[] data) {
        this.id = id;
        this.data = data;
        this.coder = interface_ID_Switch();
    }

    private Coder interface_ID_Switch() {
        switch (id) {
            case 0: return coder0;
            case 1: return coder1;
            default:
                throw new IllegalStateException();
        }
    }

    @CompilerControl(CompilerControl.Mode.DONT_INLINE)
    public int do_Dynamic_Interface_Ref() {
        return coder.work(data);
    }
}

这里有一些值得一提的技巧:

  1. 我们对几个Data类进行循环,当处理不同的coder时,JVM应该在调用点阻止调用(没看懂,原文为runtime should be "poisoned" at the call site65,poisoned直译为中毒,比喻方法不可用)。在上面的示例中,有两种类型的coder,ID = 0和ID = 1。
  2. 在@Benchmark方法中手动优化循环,这对解释器的性能有影响。
  3. 基准测试中的循环不影响测试结果。这样做是为了使nanobenchmark放大测试本身的性能开销,而不是基础硬件本身的开销。这种做法是危险的,它忽略了JMH的建议。这个技巧只能由经过培训的专业人员来完成,他们会验证这是否影响基准测试,例如死代码消除。

  4. 实际测试方法(do_Dynamic_Interface_Ref)使用了JMH注解,该注解可防止代码内联,这是避免死代码消除的重要前提,至少在现在的HotSpot上是这样的。它还有助于将有效测试与指令集转储中的其余代码分开。

2.4 VM模式

默认情况下,HotSpot VM提供了分层编译优化:首先,解释执行,然后使用基线编译器(baseline compiler )(也称为“client”编译器或C1)进行编译,最后使用积极优化编译器进行编译(也称为“server”编译器,或C2)。

实际情况更复杂。编译层的VM常量可以说明每个级别的特点。level = 1是纯C1,level = 4是纯C2,介于两者之间的是C1和分析。级别之间的转换由高级策略控制,你可以不断地讨论它们

虽然大多数人都关心C2的性能,但是积极优化需要构建相同的代码,这消耗大量时间,我们也关心在反常模式中的性能,其中代码尚未进入C2编译。

3.单一类型

如果你缺乏汇编经验,最好注意这一部分。
让我们从只有一个coder的场景开始,先解决这个问题,然后再深入研究更复杂的案例。在这里,我们只关注Ref的编码,其他方式几乎没有意义。

Coder0继承至AbstractCoder,这是该抽象类的唯一子类。还实现了Coder接口。虽然同时实现/继承了抽象类和接口比较奇怪,我们还是先尝试调用它,无论是通过抽象类(invokevirtual),接口(invokeinterface),或静态方法(invokestatic)。

public abstract class AbstractCoder {
    public abstract int abstractWork(byte[] data);
}

public interface Coder {
    int work(byte[] data);
}

public class Coder0 extends AbstractCoder implements Coder {
    public static int staticWork(byte[] data) {
        return data.length;
    }

    @Override
    public int work(byte[] data) {
        return data.length;
    }

    @Override
    public int abstractWork(byte[] data) {
        return data.length;
    }
}

3.1 C2

不多说,结果如下:

Benchmark                  (count)  Mode  Cnt   Score   Error  Units
One.dynamic_Abstract_Ref     10000  avgt   50  30.168 ± 0.602  us/op
One.dynamic_Interface_Ref    10000  avgt   50  31.391 ± 0.067  us/op
One.static_Ref               10000  avgt   50  29.171 ± 0.385  us/op

我们发现:静态比动态快,抽象比接口快。虽然差异看似微不足道,但在某些情况下,这可能很重要,让我们先搞清楚为什么。我们使用JMH的-prof perfasm分析器,它将 Linux perf event采样映射到反汇编。

还有其他方法可以进行底层分析和反汇编,包括但不限于自己处理VM ,使用Solaris Studio性能分析器JITWatch等。每种方法都有其优劣,需要学习如何使用正确的工具。JMH perfasm是分析本文JMH nanobenchmarks的理想选择。
说明:本文看到的汇编列表与VM实际汇编转储略有不同,它会更大,但多亏perfasm,你只需要关心热点区域。它还包含许多其他信息,包括机器地址,Java字节码指令,Java堆栈跟踪以及VM注释,请忽略这些差异,专注问题。你可以随时在你的环境中复现完整的反汇编代码。

3.1.1 C2:static_Ref

                  [Verified Entry Point]
  0.02%    0.02%    mov    %eax,-0x14000(%rsp)
  9.18%    6.63%    push   %rbp
           0.01%    sub    $0x10,%rsp
                    mov    0x10(%rsi),%r11d       ; get field $data
  9.54%    7.90%    mov    0xc(%r12,%r11,8),%eax  ; Coder0.staticWork() body
 10.79%   10.78%    add    $0x10,%rsp             ; epilogue and return
                    pop    %rbp
                    test   %eax,0x15f7c4e0(%rip)
 15.56%   21.52%    retq

这是一段简单直接的代码,这不奇怪,我们只是在调用静态方法。静态方法也是静态绑定的,这意味着我们不需要解析在JVM本应该调用的方法。我们明确知道编译时要静态调用哪些方法。这也适用于私有方法,它们也是静态可解析的。
> 怎么理解Coder0.staticWork() bodymov 0xc(%r12,%r11,8),%eax转储的是“arraylength”,这是压缩引用(compressed reference)的“解码” ,与实际读取做了合并。该指令相当于加载%r12 + %r11*8 + 0xc,其中%r12是一个基地址,%r11是压缩地址,8是压缩引用乘数,0xc是byte[]数组的length字段的偏移量。当前CPU中存在这么多的寻址模式是为了提高压缩引用的性能。如有疑问,你可以禁用压缩引用转储指令集(-XX:-UseCompressedOops)

另一个有趣的问题是关于空校验(null checks)。Java规范要求在循环byte[]的length之前检查null。我们知道length存储在byte[]实例中,因此需要使用非null实例。你能发现上面汇编中的空校验吗?不能,因为那里没有明确的空校验。那么,JVM如何符合规范或者至少不会崩溃?实际在完整的指令集转储中,“隐式异常,分派到$addr”靠的是mov指令。遇到空指针将导致CPU向进程发出SEGV信号。VM有一个SEGV处理程序处理这个,并抛出NullPointerException,将控制权传回。由于SEGV事件有一个返回地址,VM知道生成的代码中的哪一点触发了空校验。此优化极大地提高了Java无处不在的空校验的性能。

除了%rbp(堆栈指针)处理之外,剩下的是test %eax,0x15f7c4e0(%rip)。这看起来很奇怪,因为不会使用此指令设置的标志。为什么有这条指令?如果查看原始指令集转储,会看到"# poll"针对该行的注释。应该给你一个提示,简而言之,这是一个安全点safepoint轮询,是VM-Code接口的一部分。VM维护一个具有READ权限的特殊内存页表,当需要生成的代码尽快响应时,它会将权限降低到NONE,当test指令试图访问时将受到限制,从而将控制转移到限制处理程序,然后转移到VM。你可以在其他地方阅读更多关于安全点机制的信息,例如在Nitsan的“我的安全点在哪里?” 。

3.1.2。C2:dynamic_Abstract_Ref
[Verified Entry Point]
0.44% 0.65% mov %eax,-0x14000(%rsp)
7.93% 6.68% push %rbp
0.23% 0.24% sub $0x10,%rsp
1.46% 0.54% mov 0x10(%rsi),%r11d ; get field $data
7.96% 7.25% cmp 0x18(%rsi),%r12d ; null checking $abstractCoder
je NULL_CHECK ; $abstractCoder is null, bail!
2.05% 1.94% mov 0xc(%r12,%r11,8),%eax ; Coder0.abstractWork() body, arraylength
13.19% 14.35% add $0x10,%rsp ; epilogue and return
0.33% 0.28% pop %rbp
3.08% 3.91% test %eax,0x1642359a(%rip)
11.75% 15.78% retq
NULL_CHECK:

在人类的话语中,它加载data字段,它进行空检查abstractCoder,因为我们将要在其上调用虚方法,然后我们有内联体Coder0.abstractWork()。空检查的成本是我们所看到的与普通static呼叫的区别。

我们怎么弄清楚空检查的事情?好吧,我们已经知道%r12压缩引用基地址,请参阅上面的注释。我们也知道这%rsi是参考Data,并且我们正在偏离0x18 那里。你可能希望使用JOL交叉检查对象布局以确保。结合这两个观察结果,我们认为这将对象引用与基址进行比较,换句话说,检查它是否为无效。

人们可能想知道:如果我们从现场拉出一个不同的编码器会发生什么Coder0?然后,我们会继续进入内联机构Coder0,即使我们原来的程序肯定是在说别的吗?答案是:你正在查看投机优化的结果。VM知道可能只有一个子类AbstractCoder(来自类层次结构分析或CHA的知识),因此我们可能会跳过检查类型。如果我们的测试具有更复杂的层次结构AbstractCoder,那么VM将不得不假设其他东西可以进入此处。事实上,一旦我们开始处理多种类型的编码器,我们就会看到。

另一个令人费解的事情:Java支持动态加载的类!如果我们推测性地编译当前代码会发生什么,但是然后一些混蛋AbstractCoder通过网络接收另一个子类并加载它?一切都破了吗?答案是:JVM可以检测编译代码的条件何时无效,并丢弃已编译的代码。这就是JVM闪耀的地方:它们控制代码,它们控制代码运行的环境,因此可以推测性地进行优化。还记得上面关于安全点的说明吗?这是帮助VM在需要时快速从生成的代码中恢复控制的事情之一。

3.1.3。C2:dynamic_Interface_Ref
[Verified Entry Point]
0.03% 0.02% mov %eax,-0x14000(%rsp)
8.87% 7.69% push %rbp
0.06% 0.03% sub $0x20,%rsp
0.01% 0.02% mov 0x10(%rsi),%r10d ; get field $data
9.20% 10.68% mov 0x14(%rsi),%r8d ; get field $coder
0.01% 0.02% mov 0x8(%r12,%r8,8),%r11d ; get the class word for $coder
14.04% 18.00% cmp $0xf80102f6,%r11d ; compare it with Coder0
jne TYPE_CHECK_FAILED ; not our coder, bail!
7.50% 10.19% mov 0xc(%r12,%r10,8),%eax ; Coder0.work() body
1.74% 1.89% add $0x20,%rsp ; epilogue and return
pop %rbp
0.04% 0.04% test %eax,0x15fdfb4e(%rip)
10.31% 13.05% retq
TYPE_CHECK_FAILED:

请注意,CHA不在我们这边玩。当然,我们可以分析相同的接口,找出接口只有一个实现者等,但实际情况是真正的接口通常有很多实现者,因此在VM中实现和支持它不会通过合理的成本效益分析。

因此,编译的代码必须检查编码器类型,并且由此static解释针对简单情况的性能差异。它也比null检查编码器实例便宜,因为我们需要拉动并比较实际的类字。

我们怎么弄清楚正在加载的是什么?记住上面的注释,我们知道我们正在读取0x8与压缩地址偏移的内容%r8。%r8是一个Coder引用,因此我们从对象本身的8字节偏移量中提取8个字节。我们知道这就是一个字谜所在。

VM通过将类字与预编译常量进行比较来进行类型检查。该常量实际上是镜像类的本机VM结构的规范地址。由于它是规范的,并且因为VM负责不移动它,我们可以将所需的地址编译到生成的代码中,将这些检查减少到只是指针比较。

3.2。C1
如上所述,生成的代码在达到高度优化的版本之前会传递几层编译。有时你在达到最佳性能之前暂时拥有的东西也很重要,这就是为什么我们也应该研究C1,告诉VM停止1级的分层管道-XX:TieredStopAtLevel=1。

理想情况下,我们希望仅针对特定的基准测试方法限制分层编译级别,但是在较低级别上运行整个代码更简单。这是有效的,因为我们不比较C1与C2的性能,而只是研究具体编译器的行为方式。

Benchmark (count) Mode Cnt Score Error Units
One.dynamic_Abstract_Ref 10000 avgt 50 41.595 ± 0.049 us/op
One.dynamic_Interface_Ref 10000 avgt 50 36.562 ± 0.058 us/op
One.static_Ref 10000 avgt 50 26.594 ± 0.051 us/op
我们可以看到与C2情况略有不同的分布:静态情况仍然明显更快,但现在抽象情况明显慢于接口1。是什么赋予了?

3.2.1。C1:static_Ref
[Verified Entry Point]
8.79% 8.26% mov %eax,-0x14000(%rsp)
2.14% 1.57% push %rbp
0.28% 0.29% sub $0x30,%rsp
9.23% 7.44% mov 0x10(%rsi),%eax ; get field $data
0.77% 0.63% shl $0x3,%rax ; unpack it
0.35% 0.37% mov 0xc(%rax),%eax ; Coder0.work() body, arraylength
13.30% 10.77% add $0x30,%rsp ; epilogue and return
1.16% 0.61% pop %rbp
0.53% 0.32% test %eax,0x1650807f(%rip)
13.50% 13.20% retq
这些情况再次非常简单,因为我们完全知道调用目标,就像我们在同一C2案例中所做的那样。

压缩引用打包/解包代码也可能如下所示。请注意,它几乎与mov 0x10(%rsi),%eax; mov 0xc(%r12,%eax,8)when时%r12为0 相同 shl $0x3, 。如果没有其他原因可以执行移位,则指令是压缩引用解码的标记。

编译器之间代码生成的微小差异并不令人意外。令人惊讶的是,C2-ish指令选择略慢于C1-ish指令选择:

C2:29.575±(99.9%)1.116 us / op
C1:26.347±(99.9%)0.178 us / op
跟进这个差异将是有趣的,但超出了这项工作的范围。这个观察结果是对通过不同工具链进行的代码进行非常仔细比较的重要性的另一个例子。阅读 “Java vs. Scala:Divided We Fail”,获得相同效果的更有趣的例子。

3.2.2。C1:dynamic_Abstract_Ref
dynamic_Abstract_Ref():
[Verified Entry Point]
5.22% 3.71% mov %eax,-0x14000(%rsp)
1.12% 0.49% push %rbp
4.73% 4.15% sub $0x30,%rsp
0.20% 0.15% mov 0x18(%rsi),%edi ; get field $abstractCoder
0.73% 0.03% shl $0x3,%rdi ; unpack it
5.44% 4.78% mov 0x10(%rsi),%edx ; get field $data
0.02% shl $0x3,%rdx ; unpack it
0.16% 0.13% mov %rdi,%rsi
0.72% 0.04% mov $0xffffffffffffffff,%rax
4.89% 5.88% callq 0x00007f7ae1046220 ; VIRTUAL CALL to abstractWork()
6.01% 6.08% add $0x30,%rsp
0.45% 0.40% pop %rbp
0.28% 0.11% test %eax,0x17737229(%rip)
4.91% 6.09% retq

abstractWork():
[Verified Entry Point]
0.39% 0.18% mov %eax,-0x14000(%rsp)
5.39% 5.52% push %rbp
0.20% 0.13% sub $0x30,%rsp
0.53% 0.55% mov 0xc(%rdx),%eax ; arraylength
6.10% 5.79% add $0x30,%rsp ; epilogue and return
0.18% 0.09% pop %rbp
0.35% 0.46% test %eax,0x17736ee6(%rip)
0.45% 0.32% retq
首先,你必须注意该abstractWork方法未内联。这是因为C1无法找出调用的静态目标:调用目标并不简单,并且CHA在这里不适用于抽象类。

人们可以解释如下,运行-XX:+PrintCompilation -XX:+PrintInlining。对于C1编译中的这个方法,我们将看到这个“神秘”的消息:

net.shipilev.one.One::do_Dynamic_Abstract_Ref(12 bytes)
@ 8 net.shipilev.one.AbstractCoder :: abstractWork(0字节)没有静态绑定
现在,如果你在HotSpot源中搜索“无静态绑定”,你将找到C1 GraphBuilder的相关部分,这意味着如果它是静态/私有/动态或其他最终方法,则会发生此方法的内联。放置final修改器在Coder0.abstractWork 这里没有用,因为编译器会尝试验证是否AbstractCoder.abstractWork为final。CHA可能已经将我们保存在这里,但上面的块明确地将其排除在外(我不知道原因)。

有人问我mov $0xffffffffffffffff,%rax在电话会议之前做了什么。显然,这是-1双补码形式。我的推测是-1表示C1方法分派程序的空内联缓存。据称这里发射。

3.2.3。C1:dynamic_Interface_Ref
[Verified Entry Point]
7.63% 5.23% mov %eax,-0x14000(%rsp)
0.09% 0.22% push %rbp
7.13% 10.31% sub $0x30,%rsp
0.35% 0.10% mov 0x14(%rsi),%eax ; get field $coder
0.05% 0.16% shl $0x3,%rax ; unpack it
6.84% 10.14% mov 0x10(%rsi),%esi ; get field $data
0.16% 0.41% shl $0x3,%rsi ; unpack it
0.13% 0.11% cmp $0x0,%rax ; null-check
je WORK ; bail to implicit null-check handling

0.09% 0.06% mov $0x7c00817b0,%rcx ; load Coder0 metadata
6.50% 7.65% mov 0x8(%rax),%edx ; load classword for $coder
0.31% 0.38% shl $0x3,%rdx ; unpack it
0.49% 0.61% cmp 0x38(%rdx),%rcx ; check if $coder is actually Coder0
jne TYPE_CHECK_FAILED ; not? bail.
27.33% 30.35% jmpq WORK ; GO HOME COMPILER, YOU ARE DRUNK

             WORK:

0.31% 0.25% mov %rax,%rdi
0.07% 0.04% cmp (%rax),%rax ; implicit null check
0.05% mov 0xc(%rsi),%eax ; Coder0.work() body, arraylength
7.03% 7.82% add $0x30,%rsp ; epilog and return
0.19% 0.30% pop %rbp
0.08% 0.06% test %eax,0x1639b908(%rip)
0.15% 0.08% retq

            TYPE_CHECK_FAILED:
                <omitted>

界面调用或多或少地内联,但是类型检查有一个重要的舞蹈。这个方法的最大部分似乎在等待cmp 0x38(%rdx),%rcx和相关的跳转,虽然很难确切地说出问题是什么:它可能是获取缓存0x38(%rdx)(虽然在纳米基准测试中不太可能),或者有两个跳跃的奇怪管道效应互相追随。无论哪种方式,jmpq这似乎都是可疑的,因为我们也可能会堕落。这可以通过另一个窥孔优化器通过来修复,至少可以nop解决jmpq,但请记住我们正在讨论C1,其目标是快速生成代码,即使牺牲了它的质量。

我们如何计算cmp 0x38(%rdx),%rcx是一种类型检查?首先,我们知道在该指令之前%rdx寄存器包含了类字,因为我们0x8从对象本身的偏移量加载它,请参阅上面的注释。我们还知道%rdxVM中某些本机数据结构的地址。现在,弄清楚原生结构的布局有点困难。作为防弹解决方案,你可以使用Serviceability Agent执行此类任务。对于我们的研究,足以弄清楚我们将实际类与预期类进行比较,因此这是一种类型检查。

3.3。翻译员
解释器测试很有意思,因为你不希望优化编译器来拯救你。这当然是正确的,如果你运行测试-Xint,你会看到这样的事情:

Benchmark (count) Mode Cnt Score Error Units
One.dynamic_Abstract_Ref 10000 avgt 50 1149.768 ± 7.131 us/op
One.dynamic_Interface_Ref 10000 avgt 50 1154.198 ± 11.030 us/op
One.static_Ref 10000 avgt 50 1137.694 ± 7.405 us/op
似乎解释器是如此缓慢,不同呼叫本身的成本在那里淹没。但是,如果你使用JMH之类的东西来分析解释器perfasm,那么你将看到我们花费大部分时间进行堆栈敲击。通过以下方式进行工作可以提供良好的性能提升-Xint -XX:StackShadowPages=1:

Benchmark (count) Mode Cnt Score Error Units
One.dynamic_Abstract_Ref 10000 avgt 50 369.172 ± 2.024 us/op
One.dynamic_Interface_Ref 10000 avgt 50 388.112 ± 3.103 us/op
One.static_Ref 10000 avgt 50 344.355 ± 3.402 us/op
干预-XX:StackShadowPages可能会破坏JVM处理堆栈溢出错误的能力。不要盲目地在生产中使用它。了解更多关于堆栈溢出这里。我们正在减少阴影页面的数量,以放大我们所追求的效果。

与C2情况一样,静态情况更快,接口情况比抽象情况慢。让我们看看...生成的代码!我们不会在这里花太多时间,只是概述一些关键的事情。

听起来很不寻常,我们的解释器会生成代码。对于每个字节码指令,它生成一个或多个特定于平台的存根,它们一起实现抽象字节码执行机器。通过Futamura投影,人们可以轻易地说这实际上是一个模板编译器。这个实现的既定术语是“模板解释器”,因为它确实以(奇怪的,但是)直接的方式解释字节码。讨论如何调用这样的实现进一步概述了将解释器和编译器分开的行非常薄,如果它存在的话。

存根的汇编是“有趣”的阅读,我们不会去那里。相反,我们将进行差分分析,因为所有情况下的存根大致相同,并且只有它们的时间分布不同。

这是static_Ref:

.... [最热门的方法(内联后)] ................................
15.33%21.46%
13.89%8.23%
12.77%14.76%
10.07%9.69%
8.42%13.37%
7.10%5.56%
5.02%2.63%
4.14%2.90%
3.95%6.49%
3.61%1.08%
2.39%2.36%
我们有五个感兴趣的大型存根:

fast_aaccess_0,为了读取数组元素,请记住我们的基础结构代码将目标从数组中拉出来

invokevirtual,用于调用虚拟有效负载调用; 没有内联我们仍然需要打电话

method entry point,在进入有效载荷方法体之前执行

invoke return,在离开有效载荷方法体之前执行

invokestatic执行我们的有效载荷; 这个存根不是很热,因为我们大部分时间都在处理基础设施代码本身的较大问题。

为了比较,这是dynamic_Abstract_Ref:

.... [最热门的方法(内联后)] ................................
18.98%16.18%
16.82%8.74%
12.54%20.40%
12.04%10.56%
7.46%12.45%
6.44%6.24%
4.36%6.94%
3.76%2.69%
3.03%0.60%
2.32%2.27%
它与前一个不同,在某种意义上invokestatic已经消失了,取而代之的invokevirtual是我们的抽象调用,而且我们还有throw exceptionstub,它可以处理可能的NPE。这种差异解释了为什么static_Ref更快 - 出于同样的原因,它在编译版本中更快。如果呼叫接收器已知,则一切都变得容易。

dynamic_Interface_Ref大致相同,但有invokeinterfacestub:

.... [最热门的方法(内联后)] ................................
17.35%18.01%
12.93%8.52%
11.66%11.30%
11.62%9.44%
9.87%11.01%
6.74%11.34%
6.38%6.33%
3.60%2.10%
3.43%5.79%
2.62%1.81%
2.49%2.40%
2.23%2.24%
3.4。讨论我
看一下单个编码器的情况,我们已经发现了一些事情:

如果你在开发/编译时确切地知道呼叫接收器,那么静态调用它是个好主意。这种做法实际上与良好的工程设计相吻合:如果方法行为不依赖于接收器状态,那么它也可能static首先开始。

C2将尝试根据配置文件猜测接收器,但它必须按照语言规范的要求进行防御型检查和/或空检查。虽然在绝大多数情况下这是绰绰有余的,但这些强制检查的固有成本有时在非常高的放大率下可见。

在利用静态类型信息以及配置文件时,C1很草率。这可能会影响预热时间和性能时间。如果你能够在不影响编译时间的情况下实现类似C2的智能移动,这是一个悬而未决的问题。

重申一下,方法调用性能在大多数情况下都不是问题,而VM优化实际上非常紧密地缩小了差距。我们将继续观察当他们不能发生时会发生什么。

4.两种类型
现在我们学习了关于单一类型调用的一些基本内容,让我们看看JVM如何处理具有不同目标的调用。在基准测试中实现这一目标的最简单方法是修改我们的单一编码器基准以 隔离两个编码器。

@Param({"0.0", "0.1", "0.5", "0.9", "1.0"})
private double bias;

private Data[] datas;

@Setup
public void setup() {
Random r = new Random(12345);

List<Data> ts = new ArrayList<Data>();
for (int c = 0; c < count; c++) {
     byte[] contents = new byte[10];
     r.nextBytes(contents);
     byte id = (byte) (c < (bias * count) ? 1 : 0);
     ts.add(new Data(id, contents));
}
Collections.shuffle(ts, r);
datas = ts.toArray(new Data[0]);

}
这进一步使基准测试复杂化,因为现在我们必须决定要测量的编码器的分布。应该是50/50吗?应该是1/42吗?在事后,我们要介绍的bias,这将说什么比率不参数Coder0消耗分布情况,和在测量的基准0.0,0.1,0.5,0.9,以及1.0 的比率。

仅测量单个bias是基准测试错误。它有机会将基准测试放在某些特定的点条件下,这可能不是真正的代码所经历的。你应该尝试在不同的设置下查看基准测试的响应方式。参见“Nanotrimeing the Nanotime”中的更多讨论。我们的实验第一板做0.0,0.5和1.0,但我们想通所有这些案例都在一个方式或其他变质,见下文。这就是为什么我们添加了更现实的0.1和0.9。

4.1。参考测试
再一次,让我们“一次吃一口大象”,从我们已经熟悉的测试开始,Ref家族。 static_Ref这里不再适用,因为没有办法用静态方法选择两个编码器实现而没有别的(我们将看到辅助选择器后来如何执行)。这使得dynamic_Interface_Ref和 dynamic_Abstract_Ref。

4.1.1。C2:dynamic_ ... _Ref
如果我们用C2运行这些测试,我们将看到:

Benchmark (bias) (count) Mode Cnt Score Error Units
Two.dynamic_Interface_Ref 0.0 10000 avgt 50 52.701 ± 0.470 us/op
Two.dynamic_Interface_Ref 0.1 10000 avgt 50 57.760 ± 0.542 us/op
Two.dynamic_Interface_Ref 0.5 10000 avgt 50 94.744 ± 0.035 us/op
Two.dynamic_Interface_Ref 0.9 10000 avgt 50 59.046 ± 1.595 us/op
Two.dynamic_Interface_Ref 1.0 10000 avgt 50 50.857 ± 0.252 us/op

Two.dynamic_Abstract_Ref 0.0 10000 avgt 50 54.419 ± 1.733 us/op
Two.dynamic_Abstract_Ref 0.1 10000 avgt 50 58.244 ± 0.652 us/op
Two.dynamic_Abstract_Ref 0.5 10000 avgt 50 95.765 ± 0.180 us/op
Two.dynamic_Abstract_Ref 0.9 10000 avgt 50 58.618 ± 0.843 us/op
Two.dynamic_Abstract_Ref 1.0 10000 avgt 50 52.412 ± 0.185 us/op
请注意有关这些结果的一些事项。首先,表现取决于偏见; 我们已经可以推测为什么了,但我们稍后会深入探究确切的原因。其次,性能差异在1/2偏差附近是对称的,这已经为你提供了一些关于正在发生的事情的提示。第三,抽象案例和界面案例似乎表现相同。

好吧,让我们拆解一些东西吧!取dynamic_Interface_Ref与偏压 = 0.0,即,仅Coder0实例是存在于代码:

              [Verified Entry Point]

0.03% 0.07% mov %eax,-0x14000(%rsp)
8.36% 7.19% push %rbp
0.03% 0.03% sub $0x20,%rsp
mov 0x10(%rsi),%r10d ; get field $data
8.83% 10.53% mov 0x14(%rsi),%r8d ; get field $coder
0.05% 0.03% mov 0x8(%r12,%r8,8),%r11d ; load $coder.
12.54% 16.32% cmp $0xf801040a,%r11d ; check $coder is Coder0
jne SLOW_PATH ; not? bail
6.87% 9.52% mov 0xc(%r12,%r10,8),%eax ; Coder0.work() body
2.72% 2.52% add $0x20,%rsp ; epilogue and return
0.02% pop %rbp
0.03% 0.02% test %eax,0x171b1d0e(%rip)
10.09% 11.50% retq

              SLOW_PATH:
                mov    $0xffffffde,%esi       ; handle the very rare case
                mov    %r8d,%ebp
                mov    %r10d,(%rsp)
                callq  0x00007f4d310051a0     ; call into runtime <uncommon trap>

注意什么?代码看起来类似于我们之前的代码,但这次我们检查实际的类型coder,然后如果我们看到别的东西就调用JVM,而不是Coder0。虚拟机实际上足够智能,可以确定这里唯一的类型Coder0。

查看每个呼叫站点的类型配置文件有时很有启发性。它是可行的-XX:+PrintCompilation -XX:+PrintInlining(以及-XX:+TraceTypeProfile旧VM的附加功能)。对于这个测试,它确实会说只有一种类型(Coder0)被观察到:

net.shipilev.two.Data :::do_Dynamic_Interface_Ref(14 bytes)
@ 8 net.shipilev.two.Coder0 :: work(3 bytes)inline(hot)
\ - > TypeProfile(6391/6391计数)= net / shipilev / two / Coder0
What’s an "uncommon trap"? That’s another part of VM-Code interface. When compiler knows some branch/case is unlikely, it can emit a simple call back to VM instead of generating a whole lot of not-to-be-used code. This can greatly cut the compile times, as well as the generated code footprint, and provide denser hot paths, e.g. in loops. In this case, stepping on that uncommon trap may also mean our profile information about coder being only the Coder0 is outdated, and we need to re-profile, and recompile.

dynamic_Interface_Ref with bias = 0.1 is more interesting:

              [Verified Entry Point]

0.02% 0.12% mov %eax,-0x14000(%rsp)
7.98% 7.81% push %rbp
0.08% 0.07% sub $0x20,%rsp
0.64% 0.69% mov 0x10(%rsi),%r10d ; get field $data
7.62% 8.67% mov 0x14(%rsi),%r8d ; get field $coder
0.13% 0.03% mov 0x8(%r12,%r8,8),%r11d ; load $coder.
13.64% 19.45% cmp $0xf801040a,%r11d ; check $coder is Coder0
jne CODER_1 ; not? jump further
6.36% 8.34% mov 0xc(%r12,%r10,8),%eax ; Coder0.work() body

             EPILOG:

1.21% 1.07% add $0x20,%rsp ; epilog and return
0.03% pop %rbp
0.51% 0.62% test %eax,0x1874c24e(%rip)
8.85% 11.10% retq

             CODER_1:

1.18% 1.40% cmp $0xf8012585,%r11d ; check if $coder is Coder1
jne SLOW_PATH ; not? jump further
0.52% 0.58% mov 0xc(%r12,%r10,8),%eax ; Coder1.work() body
0.02% jmp EPILOG ; jump back to epilog

             SLOW_PATH:
                mov    $0xffffffc6,%esi       ; handle the very rare case
                mov    %r8d,%ebp
                mov    %r10d,(%rsp)
                callq  0x00007f2b2d0051a0     ; call into runtime <uncommon trap>

现在我们实际上有两个真正的编码器实现。但请注意,导致的代码Coder1路径被从直路径中推出,并且更常见的情况Coder0是将其放置。如果我们与运行 偏差= 0.9,的地方Coder0,并Coder1会扭转在这里,因为轮廓是这样说的,和代码制图器与它磋商。

换句话说,编译器使用基于频率的基本块布局,其基于基本块之间的转换频率将基本块放在直线路径上。这对机器有一个方便的效果:最常用的代码是线性的,它可以很好地与指令缓存和解码器配合使用。这对人类也有一个方便的效果:如果你在生成的代码中查看比较和分支序列,那么不太可能的分支可能会被扇出,并且可能的分支将会贯彻执行。

偏差= 0.5的相同测试:

              [Verified Entry Point]

1.80% 2.27% mov %eax,-0x14000(%rsp)
5.28% 5.51% push %rbp
0.47% 0.35% sub $0x20,%rsp
3.00% 3.95% mov 0x10(%rsi),%r10d ; get field $data
4.86% 6.28% mov 0x14(%rsi),%r8d ; get field $coder
0.10% 0.10% mov 0x8(%r12,%r8,8),%r11d ; load $coder.
13.92% 17.49% cmp $0xf8012585,%r11d ; check if $coder is Coder1
je CODER_1 ; jump further
4.96% 6.58% cmp $0xf801040a,%r11d ; check if $coder is Coder0
jne SLOW_PATH ; not? jump out
2.70% 2.93% mov 0xc(%r12,%r10,8),%eax ; Coder0.work() body
1.42% 0.93% jmp EPILOG ; jump to epilog

              CODER_1:

3.78% 5.48% mov 0xc(%r12,%r10,8),%eax ; Coder1.work() body

              EPILOG:

2.20% 1.77% add $0x20,%rsp ; epilog and return
2.11% 1.88% pop %rbp
2.21% 1.68% test %eax,0x15e9c93e(%rip)
4.41% 4.23% retq

              SLOW_PATH:
                mov    $0xffffffc6,%esi
                mov    %r8d,%ebp
                mov    %r10d,(%rsp)
                callq  0x00007f8737f6b1a0     ; call into runtime <uncommon trap>

现在,由于影响块布局的JVM配置文件,这可以说是一个退化的情况,其中任何一个Coder0或大小的摆动Coder1都会影响布局决策,可能导致一个很好的运行间差异。然而,从性能的角度来看,这样的分发意味着许多错误预测的分支,这将影响现代CPU的性能。

例如,如果我们在perf下运行(JMH方便地提供支持-prof perf),那么我们将看到逐渐更差的分支未命中率。8%的分支未命中率相当差,并且IPC(每周期指令)的命中率非常高。

偏见= 0.0:

197,299,178,798个周期#3.977 GHz
323,821,144,134指令每个周期#1.64个insn
47,692,252,113个分支机构#961.278 M / sec
16,565,723个分支未命中#0.03%的分支机构
偏见= 0.1:

197,078,370,315个周期#3.976 GHz
每周期295,840,052,712条指令#1.50 insn
44,917,582,623个分行#906.143 M / sec
836,907,280分支机构未命中率#1.86%
偏见= 0.5:

197,541,017,422个周期#3.977 GHz
每周期183,698,062,858指令#0.93 insns
31,674,184,694个分行#637.693 M / sec
2,619,451,036分支未命中#8.27%的所有分支机构
有人可能想知道,为什么我们不会datas在进行实验之前对阵列进行排序,这不会有帮助吗?当然,它将有助于这些测试中的分支预测,但由于三个原因,这是不好的基准测试。首先,这种“幸运”的编码器分发在现实生活中是不可能的。只要我们正在进行合成基准测试,我们就可以忍受。其次,我们需要将硬件效果纳入账户,至少为了估计我们之后的效果与我们无法控制的事物相比。

第三,如果你有一个大型排序数组,那么你很可能会在处理数组的过程中将探测器中途绊倒,因此会使类型配置文件出现偏差。想象一下10K元素的数组,前半部分填充了一种类型的元素,另一半填充了另一种元素。如果编译器在遍历第一个7.5K之后跳闸,那么“观察”类型的轮廓将是66.(6)%的类型1和33.(3)%的类型2,因此消除了实验设置。

dynamic_Abstract_Ref表现几乎完全相同,并生成几乎相同的代码,这解释了性能类似的原因dynamic_Interface_Ref。

4.1.2。C1:dynamic_ ... _Ref
Benchmark (bias) (count) Mode Cnt Score Error Units
Two.dynamic_Interface_Ref 0.0 10000 avgt 50 59.592 ± 0.379 us/op
Two.dynamic_Interface_Ref 0.1 10000 avgt 50 88.052 ± 0.352 us/op
Two.dynamic_Interface_Ref 0.5 10000 avgt 50 136.241 ± 0.426 us/op
Two.dynamic_Interface_Ref 0.9 10000 avgt 50 89.264 ± 1.142 us/op
Two.dynamic_Interface_Ref 1.0 10000 avgt 50 59.285 ± 1.478 us/op

Two.dynamic_Abstract_Ref 0.0 10000 avgt 50 57.449 ± 2.096 us/op
Two.dynamic_Abstract_Ref 0.1 10000 avgt 50 74.185 ± 1.606 us/op
Two.dynamic_Abstract_Ref 0.5 10000 avgt 50 120.458 ± 0.397 us/op
Two.dynamic_Abstract_Ref 0.9 10000 avgt 50 75.714 ± 1.695 us/op
Two.dynamic_Abstract_Ref 1.0 10000 avgt 50 58.020 ± 1.606 us/op
C1在偏差= 0.5附近经历相同的对称性,但现在Interface和Abstract情况明显不同。如果你反汇编两者,你会发现在两种情况下都没有内联调用,并且在单一类型的情况下看起来与C1版本类似。但是,还有一些热门代码,归功于执行实际vtable(虚拟调用表)和itable(接口调用表)分派的VM存根。

这就是vtable存根的样子:

              Decoding VtableStub vtbl[5]@12

1.68% 1.32% mov 0x8(%rsi),%eax ; unpack class word
3.50% 3.34% shl $0x3,%rax
1.69% 1.84% mov 0x1e0(%rax),%rbx ; select vtable
9.21% 8.17% jmpq *0x40(%rbx) ; select vtable[0x40], and jump
这就是itable存根的样子:

              Decoding VtableStub itbl[0]@12

1.86% 0.90% mov 0x8(%rsi),%r10d ; unpack classword
1.80% 1.73% shl $0x3,%r10
1.07% 0.71% mov 0x120(%r10),%r11d ; select interface table
7.27% 5.22% lea 0x1b8(%r10,%r11,8),%r11 ; ...and lookup the interface below
4.09% 4.13% lea (%r10),%r10
0.03% 0.06% mov (%r11),%rbx
7.98% 12.49% cmp %rbx,%rax
test %rbx,%rbx
je 0x00007f9c4d117cca
mov (%r11),%rbx
cmp %rbx,%rax
jne 0x00007f9c4d117caa
2.55% 2.60% mov 0x8(%r11),%r11d
0.98% 1.48% mov (%r10,%r11,1),%rbx
8.43% 11.33% jmpq *0x40(%rbx) ; select target method, and jump
请注意,接口选择通常较大,因为分派接口调用需要更多的工作,这解释了dynamic_Interface_Ref和之间的性能差异dynamic_Abstract_Ref。

接口调用很有意思,你必须处理分派。虚拟调用很容易,因为我们可以在现场构建整个类层次结构的vtable,并且只有一个间接关闭vtable来解析调用目标。但是,接口调用更复杂:我们只观察接口klass,而不是实现类,因此我们无法预测具体类vtable中的哪个偏移量来查找接口调用目标。

因此,我们必须查询给定对象引用的接口表,找出该特定接口的vtable,然后调用已解析的vtable。接口表位于每个类中的通用位置,因此itable(接口表)存根可以在那里查找。你可以在HotSpot源中了解有关vtable和itables的JVM表示的更多信息。

4.1.3。翻译员
Benchmark (bias) (count) Mode Cnt Score Error Units
Two.dynamic_Interface_Ref 0.0 10000 avgt 50 477.109 ± 9.907 us/op
Two.dynamic_Interface_Ref 0.1 10000 avgt 50 469.529 ± 2.929 us/op
Two.dynamic_Interface_Ref 0.5 10000 avgt 50 482.693 ± 5.955 us/op
Two.dynamic_Interface_Ref 0.9 10000 avgt 50 473.310 ± 4.758 us/op
Two.dynamic_Interface_Ref 1.0 10000 avgt 50 480.534 ± 4.382 us/op

Two.dynamic_Abstract_Ref 0.0 10000 avgt 50 454.018 ± 1.001 us/op
Two.dynamic_Abstract_Ref 0.1 10000 avgt 50 459.251 ± 0.366 us/op
Two.dynamic_Abstract_Ref 0.5 10000 avgt 50 458.424 ± 2.020 us/op
Two.dynamic_Abstract_Ref 0.9 10000 avgt 50 464.937 ± 7.386 us/op
Two.dynamic_Abstract_Ref 1.0 10000 avgt 50 454.475 ± 1.075 us/op
该之间的差异invokevirtual和invokeinterface分派也解释了为什么解释测试 dynamic_Abstract_Ref比快dynamic_Interface_Ref。我们不会进一步深入研究这一点。有兴趣的读者可以在他们认为重要的情况下描述这些情景。

4.1.4。讨论II.1
我们已经看到,两种类型的分派成本在编译器和解释器之间差异很大。

最常见的可观察到的效果之间的差异invokevirtual和invokeinterface。虽然良好的工程实践告诉我们尽可能使用接口(特别是lambdas的功能接口),但普通虚拟调用可以更便宜,因为它们不涉及复杂的调用目标查找。但是,对于大多数用C2编译的代码而言,这不是直接关注的问题。但C1和解释演示通过调用之间的可测量的差异invokevirtual和invokeinterface。

C2根据观察到的类型配置文件进行有趣的配置文件引导优化。如果只有一个接收器类型(即,呼叫站点是单态的),它可以简单地检查预测的类型,并直接内联目标。如果观察到两种接收器类型(即,呼叫站点是双态的),则可以并且将应用相同的优化,代价是两个分支。

即使具有侵略性的双态内联,硬件也存在错误预测分支的问题,如果类型是非均匀分布的,则不可避免地存在这些问题。然而,这仍然比通过vtable / itable存根更好,并且在那里经历了相同的误预测,如通过比较C2和C1所证明的那样。

4.2。欺骗JVM
好了,既然我们知道通过虚拟和接口调用进行泛型分派的问题,我们可以作弊吗?我们已经知道大多数编译器甚至解释器都能完美地解决静态调用问题。需要注意的是,我们不能在静态方法上分派多种类型。

我们可以实现辅助选择器吗?也就是说,我们可以在Data类本身中存储一些字段,并自己分派静态调用吗? 让我们实现这些:

@CompilerControl(CompilerControl.Mode.DONT_INLINE)
public int do_Static_ID_switch() {
switch (id) {
case 0: return Coder0.staticWork(data);
case 1: return Coder1.staticWork(data);
default:
throw new IllegalStateException();
}
}

@CompilerControl(CompilerControl.Mode.DONT_INLINE)
public int do_Static_ID_ifElse() {
if (id == 0) {
return Coder0.staticWork(data);
} else if (id == 1) {
return Coder1.staticWork(data);
} else {
throw new IllegalStateException();
}
}

@CompilerControl(CompilerControl.Mode.DONT_INLINE)
public int do_Static_Bool_ifElse() {
if (isCoder0) {
return Coder0.staticWork(data);
} else {
return Coder1.staticWork(data);
}
}
...并与最着名的动态分派方式进行比较,dynamic_Abstract_Ref。

4.2.1。C2:static_ ...
Benchmark (bias) (count) Mode Cnt Score Error Units

Reference

Two.dynamic_Abstract_Ref 0.0 10000 avgt 50 54.419 ± 1.733 us/op
Two.dynamic_Abstract_Ref 0.1 10000 avgt 50 58.244 ± 0.652 us/op
Two.dynamic_Abstract_Ref 0.5 10000 avgt 50 95.765 ± 0.180 us/op
Two.dynamic_Abstract_Ref 0.9 10000 avgt 50 58.618 ± 0.843 us/op
Two.dynamic_Abstract_Ref 1.0 10000 avgt 50 52.412 ± 0.185 us/op

if (coder0) Coder0.staticWork(data) else Coder1.staticWork(data);

Two.static_Bool_ifElse 0.0 10000 avgt 50 50.206 ± 0.730 us/op
Two.static_Bool_ifElse 0.1 10000 avgt 50 55.036 ± 0.686 us/op
Two.static_Bool_ifElse 0.5 10000 avgt 50 88.445 ± 0.508 us/op
Two.static_Bool_ifElse 0.9 10000 avgt 50 54.730 ± 0.733 us/op
Two.static_Bool_ifElse 1.0 10000 avgt 50 48.966 ± 1.338 us/op

if (id == 0) Coder0.staticWork(data) else if (id == 1) Coder1.staticWork(data) ...

Two.static_ID_ifElse 0.0 10000 avgt 50 48.506 ± 0.274 us/op
Two.static_ID_ifElse 0.1 10000 avgt 50 56.479 ± 1.092 us/op
Two.static_ID_ifElse 0.5 10000 avgt 50 88.368 ± 0.205 us/op
Two.static_ID_ifElse 0.9 10000 avgt 50 55.940 ± 0.935 us/op
Two.static_ID_ifElse 1.0 10000 avgt 50 48.090 ± 0.406 us/op

switch (id) case 0: Coder0.staticWork(data); break; case 1: Coder1.staticWork(data); break;

Two.static_ID_switch 0.0 10000 avgt 50 50.641 ± 2.770 us/op
Two.static_ID_switch 0.1 10000 avgt 50 57.856 ± 1.156 us/op
Two.static_ID_switch 0.5 10000 avgt 50 89.138 ± 0.265 us/op
Two.static_ID_switch 0.9 10000 avgt 50 56.312 ± 1.045 us/op
Two.static_ID_switch 1.0 10000 avgt 50 48.931 ± 1.009 us/op
我们可以看到,作弊有点帮助,但为什么呢?我们已经看到单形和双态内联实际上相当不错,我们怎么能胜出呢?让我们拆解一下。

这是static_Bool_ifElse和偏差= 0.0:

              [Verified Entry Point]

1.24% 0.73% mov %eax,-0x14000(%rsp)
3.01% 2.43% push %rbp
sub $0x20,%rsp
1.56% 0.76% movzbl 0xc(%rsi),%r11d ; get field $isCoder0
15.25% 20.43% test %r11d,%r11d
je SLOW_PATH ; jump out if ($isCoder0 == false)
1.12% 1.12% mov 0x10(%rsi),%r11d ; get field $data
5.42% 7.05% mov 0xc(%r12,%r11,8),%eax ; Coder0.work() body(), arraylength
37.71% 32.57% add $0x20,%rsp ; epilogue and return
1.51% 1.67% pop %rbp
0.10% test %eax,0x1814ecd6(%rip)
5.71% 4.06% retq

              SLOW_PATH:
                mov    %rsi,%rbp              ; handle the rare case
                mov    %r11d,(%rsp)
                mov    $0xffffff65,%esi
                callq  0x00007f32910051a0     ; call into <uncommon trap>

请注意它与单态内联有多相似!我们甚至在寒冷的树枝上有一个不常见的陷阱。与此dynamic_Abstract_Ref案例的唯一区别在于我们不会在比较类别词时遇到麻烦,因为它保存了一些指令。这解释了微小的改进。

static_Bool_ifElse和bias = 0.1的响应类似于改变的偏差:

              [Verified Entry Point]

1.07% 0.45% mov %eax,-0x14000(%rsp)
3.04% 3.43% push %rbp
0.03% 0.07% sub $0x10,%rsp
1.36% 0.57% mov 0x10(%rsi),%r11d ; get field $data
23.73% 31.22% movzbl 0xc(%rsi),%r10d ; get field $isCoder0
4.25% 4.70% test %r10d,%r10d
je CODER_1 ; jump out if ($isCoder0 == false)
1.98% 2.53% mov 0xc(%r12,%r11,8),%eax ; Coder0.work() body, arraylength

              EPILOG:

21.30% 17.72% add $0x10,%rsp ; epilogue and return
1.10% 1.25% pop %rbp
0.90% 0.47% test %eax,0x187a7416(%rip)
5.87% 3.99% retq

              CODER_1:

2.14% 2.03% mov 0xc(%r12,%r11,8),%eax ; Coder1.work() body, arraylength
4.90% 4.51% jmp EPILOG ; jump back
static_Bool_ifElse而偏见= 0.5甚至更紧:

              [Verified Entry Point]

0.14% 0.10% mov %eax,-0x14000(%rsp)
2.93% 2.70% push %rbp
0.02% sub $0x10,%rsp
0.17% 0.19% mov 0x10(%rsi),%r11d ; get field $data
24.07% 27.26% movzbl 0xc(%rsi),%r10d ; get field $isCoder0
3.93% 3.69% test %r10d,%r10d
jne CODER_0 ; jump out if ($isCoder0 == false)
6.05% 5.85% mov 0xc(%r12,%r11,8),%eax ; Coder1.work() body, arraylength
13.12% 16.53% jmp EPILOG ; jump to epilogue

              CODER_0:

5.95% 6.13% mov 0xc(%r12,%r11,8),%eax ; Coder0.work() body, arraylength

              EPILOG:

11.27% 15.97% add $0x10,%rsp ; epilogue and return
0.14% 0.21% pop %rbp
3.09% 1.99% test %eax,0x1815b50f(%rip)
3.10% 2.88% retq
...但这是真正的作弊:我们不是比较两次类别,而是将标志进行一次比较,然后选择另一种方法。这解释了对C2双态内联的显着改进。但即使在这种情况下,分支错误预测成本也会在不同的偏差中提供不对称性。偏见= 0.5,显然,错误预测分支的可能性最大。

4.2.2。C1:static_ ...
Benchmark (bias) (count) Mode Cnt Score Error Units

Reference

Two.dynamic_Abstract_Ref 0.0 10000 avgt 50 57.449 ± 2.096 us/op
Two.dynamic_Abstract_Ref 0.1 10000 avgt 50 74.185 ± 1.606 us/op
Two.dynamic_Abstract_Ref 0.5 10000 avgt 50 120.458 ± 0.397 us/op
Two.dynamic_Abstract_Ref 0.9 10000 avgt 50 75.714 ± 1.695 us/op
Two.dynamic_Abstract_Ref 1.0 10000 avgt 50 58.020 ± 1.606 us/op

if (coder0) Coder0.staticWork(data) else Coder1.staticWork(data);

Two.static_Bool_ifElse 0.0 10000 avgt 50 41.409 ± 0.145 us/op
Two.static_Bool_ifElse 0.1 10000 avgt 50 52.084 ± 0.410 us/op
Two.static_Bool_ifElse 0.5 10000 avgt 50 82.190 ± 1.855 us/op
Two.static_Bool_ifElse 0.9 10000 avgt 50 52.306 ± 0.423 us/op
Two.static_Bool_ifElse 1.0 10000 avgt 50 45.208 ± 0.513 us/op

if (id == 0) Coder0.staticWork(data) else if (id == 1) Coder1.staticWork(data)

Two.static_ID_ifElse 0.0 10000 avgt 50 40.777 ± 1.236 us/op
Two.static_ID_ifElse 0.1 10000 avgt 50 53.125 ± 2.200 us/op
Two.static_ID_ifElse 0.5 10000 avgt 50 83.833 ± 1.231 us/op
Two.static_ID_ifElse 0.9 10000 avgt 50 53.826 ± 0.490 us/op
Two.static_ID_ifElse 1.0 10000 avgt 50 47.484 ± 0.802 us/op

switch (id) case 0: Coder0.staticWork(data); break; case 1: Coder1.staticWork(data); break;

Two.static_ID_switch 0.0 10000 avgt 50 45.338 ± 1.404 us/op
Two.static_ID_switch 0.1 10000 avgt 50 53.087 ± 0.193 us/op
Two.static_ID_switch 0.5 10000 avgt 50 83.418 ± 0.900 us/op
Two.static_ID_switch 0.9 10000 avgt 50 54.315 ± 1.435 us/op
Two.static_ID_switch 1.0 10000 avgt 50 46.568 ± 0.446 us/op
还记得C1不能正确地内联abstract和interface双态调用。这就是为什么我们的狡猾的派遣方式显然是赢了。让我们通过一些反汇编来确认。

Two.do_Static_Bool_ifElse,bias = 0.0:

              [Verified Entry Point]

3.20% 3.99% mov %eax,-0x14000(%rsp)
2.34% 2.78% push %rbp
2.23% 1.68% sub $0x30,%rsp
1.29% 2.36% movsbl 0xc(%rsi),%eax ; get field $coder0
4.58% 4.19% cmp $0x0,%eax
je CODER_0 ; jump out if ($isCoder0 == false)
2.59% 1.63% mov 0x10(%rsi),%eax ; get field $data
1.91% 1.92% shl $0x3,%rax ; unpack it
0.48% 0.91% mov 0xc(%rax),%eax ; Coder0.staticWork() body, arraylength
36.12% 30.38% add $0x30,%rsp ; epilogue and return
1.07% 0.45% pop %rbp
0.32% 0.86% test %eax,0x16b80b92(%rip)
5.06% 3.71% retq

              CODER_0:
                mov    0x10(%rsi),%eax
                shl    $0x3,%rax             ; handle ($isCoder0 == false) case
                mov    0xc(%rax),%eax        ; Coder1.staticWork() body, arraylength
                ...

C1不会发出不常见的陷阱,并生成两个分支。基于频率的基本块布局仍然使最频繁的代码路径更高。请注意两者Coder0和Coder1调用是如何内联的。

do_Static_Bool_ifElse并且bias = 0.5:

              [Verified Entry Point]

1.89% 2.80% mov %eax,-0x14000(%rsp)
0.91% 0.65% push %rbp
1.84% 1.99% sub $0x30,%rsp
0.24% 0.30% movsbl 0xc(%rsi),%eax ; get field $coder0
1.97% 1.56% cmp $0x0,%eax
je CODER_1 ; jump out if ($isCoder0 == false)
3.54% 4.32% mov 0x10(%rsi),%eax ; get field $data
6.23% 7.63% shl $0x3,%rax ; unpack it
0.56% 1.26% mov 0xc(%rax),%eax ; Coder0.staticWork() body
9.48% 10.11% add $0x30,%rsp ; epilogue and return
0.09% 0.07% pop %rbp
0.07% 0.06% test %eax,0x16982ed2(%rip)
2.17% 1.82% retq

              CODER_1:

3.99% 4.16% mov 0x10(%rsi),%eax ; get field $data
5.14% 7.07% shl $0x3,%rax ; unpack it
0.82% 1.32% mov 0xc(%rax),%eax ; Coder1.staticWork() body
13.50% 13.58% add $0x30,%rsp ; epilogue and return
0.13% pop %rbp
0.06% test %eax,0x16982ebc(%rip)
2.50% 1.60% retq
这段代码在我看来更美观:只有一个测试,两个完整的分支。如果全局代码运动在分支之前移动$ data的getfield会更好,但同样,C1的目标是快速生成代码。

4.2.3。翻译员
Benchmark (bias) (count) Mode Cnt Score Error Units

Reference

Two.dynamic_Abstract_Ref 0.0 10000 avgt 50 454.018 ± 1.001 us/op
Two.dynamic_Abstract_Ref 0.1 10000 avgt 50 459.251 ± 0.366 us/op
Two.dynamic_Abstract_Ref 0.5 10000 avgt 50 458.424 ± 2.020 us/op
Two.dynamic_Abstract_Ref 0.9 10000 avgt 50 464.937 ± 7.386 us/op
Two.dynamic_Abstract_Ref 1.0 10000 avgt 50 454.475 ± 1.075 us/op

if (coder0) Coder0.staticWork(data) else Coder1.staticWork(data);

Two.static_Bool_ifElse 0.0 10000 avgt 50 432.259 ± 1.603 us/op
Two.static_Bool_ifElse 0.1 10000 avgt 50 445.540 ± 1.603 us/op
Two.static_Bool_ifElse 0.5 10000 avgt 50 490.248 ± 1.594 us/op
Two.static_Bool_ifElse 0.9 10000 avgt 50 460.986 ± 6.560 us/op
Two.static_Bool_ifElse 1.0 10000 avgt 50 444.312 ± 1.988 us/op

if (id == 0) Coder0.staticWork(data) else if (id == 1) Coder1.staticWork(data) ...

Two.static_ID_ifElse 0.0 10000 avgt 50 435.689 ± 4.659 us/op
Two.static_ID_ifElse 0.1 10000 avgt 50 449.781 ± 2.840 us/op
Two.static_ID_ifElse 0.5 10000 avgt 50 507.018 ± 3.015 us/op
Two.static_ID_ifElse 0.9 10000 avgt 50 469.138 ± 0.913 us/op
Two.static_ID_ifElse 1.0 10000 avgt 50 461.163 ± 0.813 us/op

switch (id) case 0: Coder0.staticWork(data); break; case 1: Coder1.staticWork(data); break;

Two.static_ID_switch 0.0 10000 avgt 50 474.470 ± 1.393 us/op
Two.static_ID_switch 0.1 10000 avgt 50 476.816 ± 1.699 us/op
Two.static_ID_switch 0.5 10000 avgt 50 506.091 ± 1.520 us/op
Two.static_ID_switch 0.9 10000 avgt 50 472.006 ± 4.305 us/op
Two.static_ID_switch 1.0 10000 avgt 50 465.332 ± 2.689 us/op
唉,但这个技巧对翻译不起作用。原因实际上非常简单:对编码器静态实例的额外访问需要花费一些成本。

4.2.4。讨论II.2
正如我们预测的那样,通过手动分派工作来欺骗VM的工作原理如下:

在C2的情况下,我们通过不比较类字来节省指令流中的几个字节,并且通常产生更密集的代码。这是作弊,因为编译器必须提供去优化,处理其他类型的功能,如果有的话,以及其他类型,但我们自己的代码忽略了所有这些问题。也就是说,产生不太优化的代码不是编译器错误,它只需要更通用。

在C1情况下,我们使用static目标启用目标方法的内联。分派代码编译成类似于C2版本的内容。因此,C1和C2的性能在这里更加一致。

同样,只有在需要峰值性能的情况下才应采用这些优化。C2生成的代码相当不错。

4.3。被JVM欺骗
好的,但是如何组合辅助选择器和动态情况呢?当我们对static final常量进行动态调用时,我们肯定会为更好的内联决策做好准备吗?这是它开始变得有趣的地方。让我们再次使用C2,并dynamic_…​使用一个技巧运行其中一个测试:

private AbstractCoder abstract_Bool_IfElse() {
return (isCoder0 ? coder0 : coder1);
}

@CompilerControl(CompilerControl.Mode.DONT_INLINE)
public int do_Dynamic_Abstract_Bool_ifElse() {
return abstract_Bool_IfElse().abstractWork(data);
}
Benchmark (bias) (count) Mode Cnt Score Error Units
Two.dynamic_Abstract_Ref 0.0 10000 avgt 50 54.419 ± 1.733 us/op
Two.dynamic_Abstract_Ref 0.1 10000 avgt 50 58.244 ± 0.652 us/op
Two.dynamic_Abstract_Ref 0.5 10000 avgt 50 95.765 ± 0.180 us/op
Two.dynamic_Abstract_Ref 0.9 10000 avgt 50 58.618 ± 0.843 us/op
Two.dynamic_Abstract_Ref 1.0 10000 avgt 50 52.412 ± 0.185 us/op

Two.dynamic_Abstract_Bool_ifElse 0.0 10000 avgt 50 50.240 ± 3.273 us/op
Two.dynamic_Abstract_Bool_ifElse 0.1 10000 avgt 50 61.233 ± 0.257 us/op
Two.dynamic_Abstract_Bool_ifElse 0.5 10000 avgt 50 100.861 ± 0.792 us/op
Two.dynamic_Abstract_Bool_ifElse 0.9 10000 avgt 50 60.693 ± 0.119 us/op
Two.dynamic_Abstract_Bool_ifElse 1.0 10000 avgt 50 54.018 ± 1.370 us/op
使用bias = 0.0,生成的代码看起来非常优化。

              [Verified Entry Point]

1.41% 0.97% mov %eax,-0x14000(%rsp)
3.47% 2.98% push %rbp
0.02% 0.02% sub $0x20,%rsp
1.31% 0.77% movzbl 0xc(%rsi),%r11d ; get field $isCoder0
17.88% 24.63% test %r11d,%r11d
je SLOW_PATH ; jump out if ($isCoder0 == false)
1.58% 1.72% mov 0x10(%rsi),%r11d ; get field $data
3.93% 4.99% mov 0xc(%r12,%r11,8),%eax ; Coder0.abstractWork() body, arraylength
32.66% 27.46% add $0x20,%rsp ; epilog and return
1.43% 1.60% pop %rbp
0.02% test %eax,0x17cdcad6(%rip)
6.41% 5.08% retq

              SLOW_PATH:
                mov    %rsi,%rbp
                mov    %r11d,(%rsp)
                mov    $0xffffff65,%esi
                callq  0x00007f92150051a0     ; <uncommon trap>

实际上,它几乎与static_Bool_isElse生成的代码相同!像这样的愚蠢的性能测试将使你无法相信,当一个足够智能的编译器出现时,这个技巧就不会给你带来任何成本。很明显你不应该依赖于这个,如果你尝试使用非平凡偏差,比如偏差= 0.5:

              [Verified Entry Point]

0.37% 0.77% mov %eax,-0x14000(%rsp)
1.84% 2.44% push %rbp
0.13% 0.12% sub $0x20,%rsp
0.40% 0.77% mov 0x10(%rsi),%r11d ; get field $data
16.73% 16.54% movzbl 0xc(%rsi),%r10d ; get field $isCoder0
2.04% 2.25% mov $0x71943eae0,%r8 ; get static field Data.coder1
0.13% 0.25% mov $0x71943ead0,%r9 ; get static field Data.coder0
0.10% 0.22% test %r10d,%r10d ; select either field based on $isCoder
2.01% 1.79% cmovne %r9,%r8
2.24% 1.77% mov 0x8(%r8),%r10d ; load coder.
8.38% 9.45% cmp $0xf801040a,%r10d ; if coder is Coder0
je CODER_0 ; jump over
5.65% 9.22% cmp $0xf8012585,%r10d ; if coder is Coder1
jne SLOW_PATH ; not? jump to slow path
3.39% 3.74% mov 0xc(%r12,%r11,8),%eax ; Coder1.abstractWork() body, arraylength
10.31% 9.37% jmp EPILOG ; jump to epilog

              CODER_0:

5.18% 8.45% mov 0xc(%r12,%r11,8),%eax ; Coder0.abstractWork() body, arraylength

              EPILOG:

16.63% 15.14% add $0x20,%rsp ; epilog and return
0.17% 0.27% pop %rbp
1.92% 0.47% test %eax,0x188e7ce3(%rip)
2.64% 2.05% retq

              SLOW_PATH:
                mov    $0xffffffc6,%esi
                mov    %r8,%rbp
                mov    %r11d,(%rsp)
                nop
                callq  0x00007f3dc10051a0     ; <uncommon trap>

看看发生了什么?我们做双重工作:首先,我们根据标志选择静态字段,就像我们的Java代码那样; 那么,我们正在根据实际类型进行双态内联。这与我们可以从static final常数本身推断出实际类型无关。诀窍不起作用,与dynamic_Ref案例相比,它实际上降低了性能。

4.4。模仿内联
一个不耐烦的读者可能想知道如果我们用显式instanceof检查模拟类型检查会发生什么?我们可以轻松添加一些 像这样分派的测试,例如:

@CompilerControl(CompilerControl.Mode.DONT_INLINE)
public int do_Static_Interface_Ref_ifElse() {
if (coder instanceof Coder0) {
return Coder0.staticWork(data);
} else if (coder instanceof Coder1) {
return Coder1.staticWork(data);
} else {
throw new IllegalStateException();
}
}
当然,我们事先并不知道性能是否受到instanceof反对 abstract或interface实例的影响,因此我们需要检查两者。同样,我们不知道在检查后调用 动态方法instanceof是否会很好地优化,所以我们也必须尝试调用静态方法。

4.4.1。C2
Benchmark (bias) (count) Mode Cnt Score Error Units

; abstractCoder.abstractWork(data)
Two.dynamic_Abstract_Ref 0.0 10000 avgt 50 54.419 ± 1.733 us/op
Two.dynamic_Abstract_Ref 0.1 10000 avgt 50 58.244 ± 0.652 us/op
Two.dynamic_Abstract_Ref 0.5 10000 avgt 50 95.765 ± 0.180 us/op
Two.dynamic_Abstract_Ref 0.9 10000 avgt 50 58.618 ± 0.843 us/op
Two.dynamic_Abstract_Ref 1.0 10000 avgt 50 52.412 ± 0.185 us/op

; if (abstractCoder instanceof Coder0) abstractCoder.abstractWork(data) ...
Two.dynamic_Abstract_Ref_ifElse 0.0 10000 avgt 50 54.275 ± 0.209 us/op
Two.dynamic_Abstract_Ref_ifElse 0.1 10000 avgt 50 62.353 ± 1.610 us/op
Two.dynamic_Abstract_Ref_ifElse 0.5 10000 avgt 50 95.331 ± 0.252 us/op
Two.dynamic_Abstract_Ref_ifElse 0.9 10000 avgt 50 62.308 ± 0.579 us/op
Two.dynamic_Abstract_Ref_ifElse 1.0 10000 avgt 50 55.500 ± 0.423 us/op

; if (coder instanceof Coder0) coder.work(data) ...
Two.dynamic_Interface_Ref_ifElse 0.0 10000 avgt 50 52.735 ± 0.703 us/op
Two.dynamic_Interface_Ref_ifElse 0.1 10000 avgt 50 62.417 ± 0.625 us/op
Two.dynamic_Interface_Ref_ifElse 0.5 10000 avgt 50 95.351 ± 0.492 us/op
Two.dynamic_Interface_Ref_ifElse 0.9 10000 avgt 50 63.060 ± 0.774 us/op
Two.dynamic_Interface_Ref_ifElse 1.0 10000 avgt 50 55.465 ± 0.811 us/op

; if (coder instanceof Coder0) Coder0.staticWork(data) ...
Two.static_Interface_Ref_ifElse 0.0 10000 avgt 50 50.531 ± 2.636 us/op
Two.static_Interface_Ref_ifElse 0.1 10000 avgt 50 62.725 ± 1.784 us/op
Two.static_Interface_Ref_ifElse 0.5 10000 avgt 50 95.554 ± 0.481 us/op
Two.static_Interface_Ref_ifElse 0.9 10000 avgt 50 61.832 ± 0.644 us/op
Two.static_Interface_Ref_ifElse 1.0 10000 avgt 50 53.918 ± 0.730 us/op

; if (abstractCoder instanceof Coder0) Coder0.staticWork(data) ...
Two.static_Abstract_Ref_ifElse 0.0 10000 avgt 50 55.039 ± 0.425 us/op
Two.static_Abstract_Ref_ifElse 0.1 10000 avgt 50 57.879 ± 0.892 us/op
Two.static_Abstract_Ref_ifElse 0.5 10000 avgt 50 95.014 ± 0.660 us/op
Two.static_Abstract_Ref_ifElse 0.9 10000 avgt 50 57.665 ± 0.795 us/op
Two.static_Abstract_Ref_ifElse 1.0 10000 avgt 50 49.874 ± 0.183 us/op
这些结果似乎暗示两种方式都没有显着差异。如果我们反汇编一些案例以了解它们是如何编译的,那将是有启发性的

dynamic_Interface_Ref_ifElse与偏压= 0.1:

              [Verified Entry Point]
       0.01%    mov    %eax,-0x14000(%rsp)

4.22% 3.71% push %rbp
0.01% sub $0x20,%rsp
0.05% 0.21% mov 0x14(%rsi),%r11d ; get field $coder
15.77% 20.67% mov 0x8(%r12,%r11,8),%r10d ; load $coder.
7.16% 7.92% mov 0x10(%rsi),%r9d ; get field $data
0.17% 0.08% cmp $0xf8010422,%r10d ; check $coder is Coder0
jne CODER_1 ; jump further if not
1.27% 1.59% mov 0xc(%r12,%r9,8),%eax ; Coder0.work() body, arraylength

              EPILOG:

32.13% 29.84% add $0x20,%rsp ; epilogue and return
pop %rbp
0.55% 0.24% test %eax,0x18a10f8e(%rip)
4.84% 3.66% retq

              CODER_1:

1.73% 1.65% cmp $0xf8012585,%r10d ; check if $coder is Coder1
jne SLOW_PATH ; jump to slowpath if not
1.28% 1.64% mov 0xc(%r12,%r9,8),%eax ; Coder1.work() body, arraylength
3.59% 3.13% jmp EPILOG ; jump back

              SLOW_PATH:
                <omitted>

static_Abstract_Ref_ifElse与偏压= 0.1:

              [Verified Entry Point]

0.01% mov %eax,-0x14000(%rsp)
4.23% 3.68% push %rbp
0.01% sub $0x20,%rsp
0.04% 0.08% mov 0x18(%rsi),%r11d ; get field $coder
18.17% 23.09% mov 0x8(%r12,%r11,8),%r10d ; load $coder.
13.54% 16.20% mov 0x10(%rsi),%r9d ; get field $data
1.18% 1.34% cmp $0xf8010422,%r10d ; check $coder is Coder0
jne CODER_1 ; jump further if not
2.08% 2.68% mov 0xc(%r12,%r9,8),%eax ; Coder0.work() body, arraylength

              EPILOG:

19.26% 15.89% add $0x20,%rsp ; epilogue and return
pop %rbp
0.66% 0.41% test %eax,0x18b55c8e(%rip)
4.60% 3.56% retq

              CODER_1:

2.54% 2.16% cmp $0xf8012585,%r10d ; check if $coder is Coder1
jne SLOW_PATH ; jump to slowpath if not
1.67% 1.79% mov 0xc(%r12,%r9,8),%eax ; Coder1.work() body, arraylength
3.71% 2.54% jmp EPILOG ; jump back

              SLOW_PATH:
                <omitted>

不仅这些代码片段完全相同,它们也相当于让C2自己进行双态内联!让我们记住这个低级技巧,因为我们稍后在处理几种类型时会重用它。

这是可能的,因为instanceof它通常由编译器处理,并解析为与编译器本身相同的类型检查。因此,看到VM生成完全相同的代码并不奇怪。

4.4.2。C1
Benchmark (bias) (count) Mode Cnt Score Error Units

; abstractCoder.abstractWork(data)
Two.dynamic_Abstract_Ref 0.0 10000 avgt 50 57.449 ± 2.096 us/op
Two.dynamic_Abstract_Ref 0.1 10000 avgt 50 74.185 ± 1.606 us/op
Two.dynamic_Abstract_Ref 0.5 10000 avgt 50 120.458 ± 0.397 us/op
Two.dynamic_Abstract_Ref 0.9 10000 avgt 50 75.714 ± 1.695 us/op
Two.dynamic_Abstract_Ref 1.0 10000 avgt 50 58.020 ± 1.606 us/op

; if (abstractCoder instanceof Coder0) abstractCoder.abstractWork(data) ...
Two.dynamic_Abstract_Ref_ifElse 0.0 10000 avgt 50 67.736 ± 0.628 us/op
Two.dynamic_Abstract_Ref_ifElse 0.1 10000 avgt 50 78.921 ± 0.484 us/op
Two.dynamic_Abstract_Ref_ifElse 0.5 10000 avgt 50 127.833 ± 0.418 us/op
Two.dynamic_Abstract_Ref_ifElse 0.9 10000 avgt 50 87.741 ± 0.829 us/op
Two.dynamic_Abstract_Ref_ifElse 1.0 10000 avgt 50 77.055 ± 1.126 us/op

; if (coder instanceof Coder0) coder.work(data) ...
Two.dynamic_Interface_Ref_ifElse 0.0 10000 avgt 50 67.132 ± 0.557 us/op
Two.dynamic_Interface_Ref_ifElse 0.1 10000 avgt 50 81.280 ± 1.099 us/op
Two.dynamic_Interface_Ref_ifElse 0.5 10000 avgt 50 127.587 ± 0.195 us/op
Two.dynamic_Interface_Ref_ifElse 0.9 10000 avgt 50 87.288 ± 0.809 us/op
Two.dynamic_Interface_Ref_ifElse 1.0 10000 avgt 50 75.220 ± 1.172 us/op

; if (coder instanceof Coder0) Coder0.staticWork(data) ...
Two.static_Interface_Ref_ifElse 0.0 10000 avgt 50 51.127 ± 0.897 us/op
Two.static_Interface_Ref_ifElse 0.1 10000 avgt 50 60.506 ± 2.378 us/op
Two.static_Interface_Ref_ifElse 0.5 10000 avgt 50 103.250 ± 0.207 us/op
Two.static_Interface_Ref_ifElse 0.9 10000 avgt 50 69.919 ± 3.465 us/op
Two.static_Interface_Ref_ifElse 1.0 10000 avgt 50 59.107 ± 2.813 us/op

; if (abstractCoder instanceof Coder0) Coder0.staticWork(data) ...
Two.static_Abstract_Ref_ifElse 0.0 10000 avgt 50 50.748 ± 0.797 us/op
Two.static_Abstract_Ref_ifElse 0.1 10000 avgt 50 61.218 ± 0.320 us/op
Two.static_Abstract_Ref_ifElse 0.5 10000 avgt 50 103.684 ± 0.446 us/op
Two.static_Abstract_Ref_ifElse 0.9 10000 avgt 50 69.081 ± 0.104 us/op
Two.static_Abstract_Ref_ifElse 1.0 10000 avgt 50 58.921 ± 0.335 us/op
C1坦率地回应这个黑客。dynamic案例通常较慢,因为仍然涉及虚拟/接口调用,C1无法内联。static案例通常更快,因为它们避免进行虚拟/接口调用。然而,static案例与干净的“by ID”选择器并不相同,因为生成的代码的优化程度要低得多。有兴趣的读者可以进行反汇编和研究。

4.5。讨论二
看一下两个编码器的情况,我们可以发现一些事情:

在C2中很好地优化了在不同(子)类或接口实现器中分离不同行为的通常编码实践。C1在有效内联双态情况方面存在一些问题,但幸运的是,用户只会暂时观察这种行为,直到C2开始。

在某些情况下,当你完全知道调用目标时,最好静态地分配几个实现,以避免处理仍然在生成的代码中留下一些开销的编译器优化。同样,这样做可能会产生非通用的解决方案,这也是它可能更快的原因之一。编译器应该提供通用实现,因此可以帮助那么多。

我们观察到的性能成本,特别是在优化的情况下,与硬件分支(错误)预测有关。即使是最优化的代码仍然应该有分支。看起来似乎没有理由内联虚拟呼叫,但这是一个短视的观点。内联实际上扩大了其他优化的范围,在许多情况下,仅在内联就足够了。使用非常薄的纳米标记,这种优势无法得到适当的量化。

5.三种类型及其他
使用三种编码器类型,编码器分配使得任务变得更具挑战性。最简单的解决方案似乎是每个编码器类型使用不同的“部件”。

@Param("1")
private int p1;

@Param("1")
private int p2;

@Param("1")
private int p3;

private Data[] datas;

@Setup
public void setup() {
Random r = new Random(12345);

int s1 = (count * p1) / (p1 + p2 + p3);
int s2 = (count * (p1 + p2)) / (p1 + p2 + p3);

List<Data> ts = new ArrayList<Data>();
for (int c = 0; c < count; c++) {
    byte[] contents = new byte[10];
    r.nextBytes(contents);

    byte id;
    if (c < s1) {
        id = 0;
    } else if (c < s2) {
        id = 1;
    } else {
        id = 2;
    }
    ts.add(new Data(id, contents));
}
Collections.shuffle(ts, r);
datas = ts.toArray(new Data[0]);

}
此外,我们需要扩展三种类型的选择器。 这是源代码。

5.1。单形情形
请记住,在前面的部分中,编译器根据配置文件做出内联决策。当每个呼叫站点只有一个观察类型(即呼叫站点是“单态”),或每个呼叫站点两个观察到的类型(即呼叫站点是“双态”)时,至少C2能够优化这种情况。在进一步研究之前,有必要对我们的三类基准测试进行验证。通过这种方式,我们可以控制前一部分的知识可以用于此测试。

5.1.1。C2
Benchmark (count) (p1) (p2) (p3) Mode Cnt Score Error Units
Three.dynamic_Abstract_Ref 10000 1 0 0 avgt 50 52.384 ± 0.653 us/op
Three.dynamic_Interface_Ref 10000 1 0 0 avgt 50 51.807 ± 0.453 us/op
Three.static_ID_ifElse 10000 1 0 0 avgt 50 48.162 ± 0.182 us/op
Three.static_ID_switch 10000 1 0 0 avgt 50 48.597 ± 0.539 us/op

Three.dynamic_Abstract_Ref 10000 0 1 0 avgt 50 51.663 ± 0.159 us/op
Three.dynamic_Interface_Ref 10000 0 1 0 avgt 50 51.784 ± 0.824 us/op
Three.static_ID_ifElse 10000 0 1 0 avgt 50 48.308 ± 0.855 us/op
Three.static_ID_switch 10000 0 1 0 avgt 50 48.514 ± 0.148 us/op

Three.dynamic_Abstract_Ref 10000 0 0 1 avgt 50 52.356 ± 0.541 us/op
Three.dynamic_Interface_Ref 10000 0 0 1 avgt 50 52.874 ± 0.644 us/op
Three.static_ID_ifElse 10000 0 0 1 avgt 50 48.349 ± 0.200 us/op
Three.static_ID_switch 10000 0 0 1 avgt 50 48.651 ± 0.052 us/op
到目前为止,一切都在增加。我们讨论了为什么static案例比dynamic以前的案例略快。

值得注意的是,即使…​ifElse案例产生静态布局,其理论上应该受到编码器占主导地位的影响,但在实践中不会受到影响。例如,static_ID_ifElse 与P1 / P2 / P3 = 0/0/1看起来像这样:

              [Verified Entry Point]

0.06% 0.06% mov %eax,-0x14000(%rsp)
4.44% 3.30% push %rbp
0.06% 0.10% sub $0x20,%rsp
0.02% 0.03% movsbl 0xc(%rsi),%r11d ; get field $id
12.10% 16.93% test %r11d,%r11d
je ID_0 ; jump if ($id == 0)
0.65% 0.66% cmp $0x1,%r11d
je ID_1 ; jump if ($id == 1)
0.75% 0.92% cmp $0x2,%r11d
jne FAIL ; jump if ($id != 2)
0.77% 0.85% mov 0x10(%rsi),%r11d ; get field $data
4.15% 2.73% mov 0xc(%r12,%r11,8),%eax ; Coder2.staticWork() body, arraylength
45.11% 44.93% add $0x20,%rsp ; epilogue and return
0.03% pop %rbp
0.03% 0.06% test %eax,0x170e758a(%rip)
6.48% 5.66% retq

              FAIL:
                <omitted>

              ID_0:
                <omitted>

              ID_1:
                <omitted>

即使我们必须通过2个冗余分支,我们仍然保持几乎相同的性能。所有分支似乎都很好地预测。

5.1.2。C1
Benchmark (count) (p1) (p2) (p3) Mode Cnt Score Error Units
Three.dynamic_Abstract_Ref 10000 1 0 0 avgt 50 56.038 ± 2.058 us/op
Three.dynamic_Interface_Ref 10000 1 0 0 avgt 50 55.953 ± 1.388 us/op
Three.static_ID_ifElse 10000 1 0 0 avgt 50 45.050 ± 1.048 us/op
Three.static_ID_switch 10000 1 0 0 avgt 50 45.899 ± 0.199 us/op

Three.dynamic_Abstract_Ref 10000 0 1 0 avgt 50 56.541 ± 0.562 us/op
Three.dynamic_Interface_Ref 10000 0 1 0 avgt 50 55.355 ± 0.182 us/op
Three.static_ID_ifElse 10000 0 1 0 avgt 50 47.162 ± 0.469 us/op
Three.static_ID_switch 10000 0 1 0 avgt 50 46.968 ± 0.198 us/op

Three.dynamic_Abstract_Ref 10000 0 0 1 avgt 50 55.299 ± 0.118 us/op
Three.dynamic_Interface_Ref 10000 0 0 1 avgt 50 56.877 ± 0.469 us/op
Three.static_ID_ifElse 10000 0 0 1 avgt 50 46.654 ± 0.871 us/op
Three.static_ID_switch 10000 0 0 1 avgt 50 47.972 ± 0.198 us/op
在C1情况下绘制了相同的图片。我们已经知道为什么static案例比dynamic 案例稍快一些,请参阅单一和两种类型的案例。然而,在这里,类型所处的位置似乎有点重要。

2.37% 3.29% mov %eax,-0x14000(%rsp)
3.12% 2.04% push %rbp
2.12% 2.18% sub $0x40,%rsp
1.46% 0.72% movsbl 0xc(%rsi),%eax ; get field $id
3.43% 2.96% cmp $0x0,%eax
je ID_0 ; jump if ($id == 0)
2.16% 2.17% cmp $0x1,%eax
je ID_1 ; jump if ($id == 1)
0.80% 0.89% cmp $0x2,%eax
jne FAIL ; jump if ($id != 2)
1.75% 1.12% mov 0x10(%rsi),%eax ; get field $data
3.60% 3.56% shl $0x3,%rax ; unpack
1.85% 2.10% mov 0xc(%rax),%eax ; Coder2.staticWork() body, arraylength
29.12% 24.91% add $0x40,%rsp ; epilogue and return
0.71% 1.14% pop %rbp
1.44% 1.22% test %eax,0x18217420(%rip)
5.43% 5.24% retq

              FAIL:
                <omitted>

              ID_0:
                <omitted>

              ID_1:
                <omitted>

指令选择有点不同,正如人们可以从不同的编译器中看到的那样,这可以解释编译的代码版本之间的性能差异,以及为什么C1版本对代码所采用的分支数量作出反应的原因。

我自己的推测是这样的:cmp $0x0, %eax实际上比它慢一点test %eax, %eax。这些微小的微观架构差异有时是纳米基准测试的全部。

5.2。双态情况
5.2.1。C2
Benchmark (count) (p1) (p2) (p3) Mode Cnt Score Error Units
Three.dynamic_Abstract_Ref 10000 0 1 1 avgt 50 95.986 ± 0.835 us/op
Three.dynamic_Interface_Ref 10000 0 1 1 avgt 50 97.242 ± 2.292 us/op
Three.static_ID_ifElse 10000 0 1 1 avgt 50 89.957 ± 0.164 us/op
Three.static_ID_switch 10000 0 1 1 avgt 50 92.412 ± 2.351 us/op

Three.dynamic_Abstract_Ref 10000 1 0 1 avgt 50 97.081 ± 1.534 us/op
Three.dynamic_Interface_Ref 10000 1 0 1 avgt 50 97.001 ± 2.200 us/op
Three.static_ID_ifElse 10000 1 0 1 avgt 50 89.764 ± 1.230 us/op
Three.static_ID_switch 10000 1 0 1 avgt 50 92.158 ± 1.512 us/op

Three.dynamic_Abstract_Ref 10000 1 1 0 avgt 50 96.243 ± 1.528 us/op
Three.dynamic_Interface_Ref 10000 1 1 0 avgt 50 95.130 ± 0.234 us/op
Three.static_ID_ifElse 10000 1 1 0 avgt 50 88.195 ± 0.296 us/op
Three.static_ID_switch 10000 1 1 0 avgt 50 89.794 ± 0.124 us/op
同样的情况适用于双态情况。我们已经知道C2能够进行双态内联。我们也知道我们可以通过自己的分派来欺骗并节省几个周期。

5.2.2。C1
Benchmark (count) (p1) (p2) (p3) Mode Cnt Score Error Units
Three.dynamic_Abstract_Ref 10000 0 1 1 avgt 50 119.439 ± 0.371 us/op
Three.dynamic_Interface_Ref 10000 0 1 1 avgt 50 136.081 ± 0.605 us/op
Three.static_ID_ifElse 10000 0 1 1 avgt 50 84.079 ± 0.218 us/op
Three.static_ID_switch 10000 0 1 1 avgt 50 84.402 ± 0.860 us/op

Three.dynamic_Abstract_Ref 10000 1 0 1 avgt 50 120.241 ± 0.617 us/op
Three.dynamic_Interface_Ref 10000 1 0 1 avgt 50 136.434 ± 0.673 us/op
Three.static_ID_ifElse 10000 1 0 1 avgt 50 82.849 ± 0.295 us/op
Three.static_ID_switch 10000 1 0 1 avgt 50 83.061 ± 0.401 us/op

Three.dynamic_Abstract_Ref 10000 1 1 0 avgt 50 119.770 ± 0.216 us/op
Three.dynamic_Interface_Ref 10000 1 1 0 avgt 50 137.624 ± 3.552 us/op
Three.static_ID_ifElse 10000 1 1 0 avgt 50 85.120 ± 0.147 us/op
Three.static_ID_switch 10000 1 1 0 avgt 50 84.840 ± 0.218 us/op
再说一遍,这里没什么新意 我们已经知道C1无法进行适当的双态内联,而且我们仍然遇到非最佳代码。我们的自定义分派启用内联,并避免VM分派。

5.3。变形的案例
现在,当有两种以上的类型时,我们正在采取新的措施。为了使事情变得有趣,我们测量了三种配置:均匀分布类型(1/1/1),第一种类型占所有实例的90%(18/1/1),第一种类型占95%所有实例(38/1/1)。

5.3.1。C2
Benchmark (count) (p1) (p2) (p3) Mode Cnt Score Error Units
Three.dynamic_Abstract_Ref 10000 1 1 1 avgt 50 138.611 ± 2.524 us/op
Three.dynamic_Interface_Ref 10000 1 1 1 avgt 50 155.505 ± 3.027 us/op
Three.static_ID_ifElse 10000 1 1 1 avgt 50 99.868 ± 2.026 us/op
Three.static_ID_switch 10000 1 1 1 avgt 50 98.905 ± 0.600 us/op

Three.dynamic_Abstract_Ref 10000 18 1 1 avgt 50 79.674 ± 2.423 us/op
Three.dynamic_Interface_Ref 10000 18 1 1 avgt 50 90.016 ± 0.940 us/op
Three.static_ID_ifElse 10000 18 1 1 avgt 50 57.578 ± 1.079 us/op
Three.static_ID_switch 10000 18 1 1 avgt 50 55.561 ± 0.169 us/op

Three.dynamic_Abstract_Ref 10000 38 1 1 avgt 50 58.465 ± 0.160 us/op
Three.dynamic_Interface_Ref 10000 38 1 1 avgt 50 58.335 ± 0.209 us/op
Three.static_ID_ifElse 10000 38 1 1 avgt 50 51.692 ± 1.070 us/op
Three.static_ID_switch 10000 38 1 1 avgt 50 51.860 ± 0.156 us/op
我们在这里能看到什么?

static当跑步偏向一种类型时,情况会好转。这是由于更好的分支预测。这种差异提供了对优化的虚拟呼叫应该期望的估计。(我们另外验证了当我们将分布偏向任何其他类型时产生的结果完全相同,而不仅仅是第一种)。

当类型均匀分布时,我们会遇到严重的性能损失dynamic_…​。这是因为HotSpot认为呼叫站点现在有太多的接收器类型; 换句话说,呼叫站点是变形的。当前C2根本不进行变形内联。

......除了一个案例,当有明显的赢家时,声称> 90%的类型档案。

为什么我们尝试过38/1/1?此分配将95%的目标分配给单个编码器,我们知道 C2实现 将此乐观内联的阈值设置为TypeProfileMajorReceiverPercent90%。

简要介绍一下,并dynamic_Abstract_Ref以18/1/1的分解结果:

             [Verified Entry Point]

0.52% 0.53% mov %eax,-0x14000(%rsp)
2.50% 2.89% push %rbp
sub $0x10,%rsp
2.37% 1.20% mov 0x10(%rsi),%r10d ; get field $data
7.67% 9.87% mov 0x18(%rsi),%r8d ; get field $abstractCoder
1.70% 1.88% mov %r10,%rdx
0.37% 0.24% shl $0x3,%rdx
1.33% 0.60% mov %r8,%rsi
0.63% 1.29% shl $0x3,%rsi
0.51% 0.63% xchg %ax,%ax
0.43% 0.26% mov $0xffffffffffffffff,%rax
1.46% 0.48% callq 0x00007f4bd1046220 ; VIRTUAL CALL TO abstractWork()
2.96% 0.38% add $0x10,%rsp
0.79% 0.94% pop %rbp
1.44% 1.22% test %eax,0x16921d01(%rip)
0.86% 0.12% retq
...以及所有未在配置文件中内联的编码器:

.... [最热门方法(内联后)] ...................................... ........................
34.02%37.66%net.shipilev.three.Coder0 :: abstractWork
25.20%22.80%net.shipilev.three.Data :::_Dynamic_Abstract_Ref
14.79%15.94%net.shipilev.three..generated.Three_dynamic_Abstract_Ref :: dynamic_Abstract_Ref_avgt_jmhStub
13.60%13.36%java.util.HashMap :: hash
4.54%3.35%net.shipilev.three.Coder2 :: abstractWork
4.05%2.98%net.shipilev.three.Coder1 :: abstractWork
这与我们在C1案例中看到的类似。

dynamic_Abstract_Ref在38/1/1拆卸显示Coder0.abstractWork()内联!

              [Verified Entry Point]
                mov    %eax,-0x14000(%rsp)

3.98% 3.41% push %rbp
0.01% sub $0x10,%rsp
mov 0x10(%rsi),%r11d ; get field $data
10.59% 14.23% mov 0x18(%rsi),%r10d ; get field $abstractCoder
1.96% 2.51% mov 0x8(%r12,%r10,8),%r9d ; load $abstractCoder.
3.54% 4.89% cmp $0xf80103f6,%r9d ;
jne SLOW_PATH ; jump if $abstractCoder is not Coder0
0.98% 1.20% mov 0xc(%r12,%r11,8),%eax ; Coder0.abstractWork() body, arraylength
42.14% 41.75% add $0x10,%rsp ; epilogue and return
0.01% pop %rbp
0.01% test %eax,0x15ed3f4e(%rip)
4.77% 3.80% retq

              SLOW_PATH:

0.56% 0.66% mov %r11,%rdx
0.22% 0.20% shl $0x3,%rdx
0.43% 0.35% lea (%r12,%r10,8),%rsi
xchg %ax,%ax
0.01% 0.01% mov $0xffffffffffffffff,%rax
0.21% 0.19% callq 0x00007f564c3c3220 ; VIRTUAL CALL TO abstractWork()
...并且没有明显Coder0::abstractWork的个人资料:

.... [最热门方法(内联后)] ...................................... ........................
70.49%73.34%net.shipilev.three.Data :::_Dynamic_Abstract_Ref
21.47%20.44%net.shipilev.three.generated.Three_dynamic_Abstract_Ref :: dynamic_Abstract_Ref_avgt_jmhStub
2.12%1.22%net.shipilev.three.Coder1 :: abstractWork
2.05%1.25%net.shipilev.three.Coder2 :: abstractWork
2.04%2.12%java.util.concurrent.ConcurrentHashMap :: tabAt
5.3.2。C1
Benchmark (count) (p1) (p2) (p3) Mode Cnt Score Error Units
Three.dynamic_Abstract_Ref 10000 1 1 1 avgt 50 138.515 ± 0.595 us/op
Three.dynamic_Interface_Ref 10000 1 1 1 avgt 50 153.387 ± 0.184 us/op
Three.static_ID_ifElse 10000 1 1 1 avgt 50 91.239 ± 0.170 us/op
Three.static_ID_switch 10000 1 1 1 avgt 50 91.631 ± 0.821 us/op

Three.dynamic_Abstract_Ref 10000 18 1 1 avgt 50 72.798 ± 0.205 us/op
Three.dynamic_Interface_Ref 10000 18 1 1 avgt 50 88.188 ± 0.180 us/op
Three.static_ID_ifElse 10000 18 1 1 avgt 50 54.326 ± 0.393 us/op
Three.static_ID_switch 10000 18 1 1 avgt 50 54.454 ± 0.081 us/op

Three.dynamic_Abstract_Ref 10000 38 1 1 avgt 50 67.391 ± 0.928 us/op
Three.dynamic_Interface_Ref 10000 38 1 1 avgt 50 81.523 ± 0.192 us/op
Three.static_ID_ifElse 10000 38 1 1 avgt 50 50.885 ± 0.064 us/op
Three.static_ID_switch 10000 38 1 1 avgt 50 50.374 ± 0.154 us/op
static案件的行为与C2案件相同。dynamic无论概要信息如何,都不会内联案例。我们已经看到它甚至没有内联双态调用,并且自然也没有内联多变量调用。不同分布之间的差异可以通过VM存根或我们自己的静态分派程序中的分支预测来解释。

5.4。欺骗JVM
我们已经static在本节中对案例作了欺骗,但请记住这个instanceof伎俩。我们知道做手册instanceof就像做VM工作一样。我们可以在这里扮演上帝,并在instanceof 支票下剥离第一个编码器吗?换句话说,做这样的事情:

public int do_Peel_Interface_Static() {
if (coder instanceof Coder0) {
return Coder0.staticWork(data);
} else {
return coder.work(data);
}
}

public int do_Peel_Interface_Interface() {
if (coder instanceof Coder0) {
return coder.work(data);
} else {
return coder.work(data);
}
}
5.4.1。C2
Benchmark (count) (p1) (p2) (p3) Mode Cnt Score Error Units

; equidistributed
Three.dynamic_Abstract_Ref 10000 1 1 1 avgt 50 138.611 ± 2.524 us/op
Three.dynamic_Interface_Ref 10000 1 1 1 avgt 50 155.505 ± 3.027 us/op
Three.static_ID_ifElse 10000 1 1 1 avgt 50 99.868 ± 2.026 us/op
Three.static_ID_switch 10000 1 1 1 avgt 50 98.905 ± 0.600 us/op

Three.peel_Abstract_Abstract 10000 1 1 1 avgt 50 105.511 ± 1.733 us/op
Three.peel_Abstract_Static 10000 1 1 1 avgt 50 105.814 ± 1.177 us/op
Three.peel_Interface_Interface 10000 1 1 1 avgt 50 105.841 ± 1.694 us/op
Three.peel_Interface_Static 10000 1 1 1 avgt 50 105.594 ± 1.151 us/op

; biased towards coder0 at 90%
Three.dynamic_Abstract_Ref 10000 18 1 1 avgt 50 79.674 ± 2.423 us/op
Three.dynamic_Interface_Ref 10000 18 1 1 avgt 50 90.016 ± 0.940 us/op
Three.static_ID_ifElse 10000 18 1 1 avgt 50 57.578 ± 1.079 us/op
Three.static_ID_switch 10000 18 1 1 avgt 50 55.561 ± 0.169 us/op

Three.peel_Abstract_Abstract 10000 18 1 1 avgt 50 63.588 ± 2.789 us/op
Three.peel_Abstract_Static 10000 18 1 1 avgt 50 63.164 ± 1.655 us/op
Three.peel_Interface_Interface 10000 18 1 1 avgt 50 64.309 ± 2.956 us/op
Three.peel_Interface_Static 10000 18 1 1 avgt 50 62.016 ± 0.185 us/op

; biased towards coder0 at 95%
Three.dynamic_Abstract_Ref 10000 38 1 1 avgt 50 58.465 ± 0.160 us/op
Three.dynamic_Interface_Ref 10000 38 1 1 avgt 50 58.335 ± 0.209 us/op
Three.static_ID_ifElse 10000 38 1 1 avgt 50 51.692 ± 1.070 us/op
Three.static_ID_switch 10000 38 1 1 avgt 50 51.860 ± 0.156 us/op

Three.peel_Abstract_Abstract 10000 38 1 1 avgt 50 57.915 ± 0.174 us/op
Three.peel_Abstract_Static 10000 38 1 1 avgt 50 58.093 ± 0.263 us/op
Three.peel_Interface_Interface 10000 38 1 1 avgt 50 58.642 ± 1.596 us/op
Three.peel_Interface_Static 10000 38 1 1 avgt 50 57.859 ± 0.198 us/op
我们在这看到什么?好吧,事实证明,在1/1/1的真正变形的情况下,剥离第一个编码器显然是有利可图的。18/1/1的分布也是有利可图的。并且它在38/1/1的非常偏斜的分布上停止盈利。你能猜到为什么吗?你应该已经学到足够的知识来提供一个合理的假设。如果可以,那我的工作就在这里完成。

所以,如果我们看看peel_Interface_Interface在1/1/1:

              [Verified Entry Point]

0.23% 0.20% mov %eax,-0x14000(%rsp)
2.14% 2.45% push %rbp
0.11% 0.06% sub $0x20,%rsp
0.20% 0.54% mov 0x14(%rsi),%r11d ; get field $coder
19.68% 21.62% mov 0x8(%r12,%r11,8),%r10d ; load $coder.
7.74% 10.11% mov 0x10(%rsi),%ebp ; get field $data
0.17% 0.15% cmp $0xf8010426,%r10d ; check if $coder == Coder0
jne OTHER_CODERS ; jump further if not
5.95% 8.92% mov 0xc(%r12,%rbp,8),%eax ; Coder0.work() body, arraylength

              EPILOG:

9.35% 8.89% add $0x20,%rsp ; epilogue and return
0.10% 0.05% pop %rbp
2.89% 1.11% test %eax,0x15f1f36f(%rip)
2.16% 1.32% retq

              OTHER_CODERS:

2.63% 3.82% cmp $0xf80125c8,%r10d ; check if $coder == Coder2
je CODER_2 ; jump over if so
3.53% 4.68% cmp $0xf8012585,%r10d ; check if $coder == Coder1
jne SLOW_PATH ; jump to slow path if not
2.38% 2.17% mov 0xc(%r12,%rbp,8),%eax ; Coder1.work() body, arraylength
10.51% 10.05% jmp EPILOG ; jump back to epilogue

              CODER_2:

2.92% 4.08% mov 0xc(%r12,%rbp,8),%eax ; Coder2.work() body, arraylength
5.34% 5.70% jmp EPILOG ; jump back to epilogue

              SLOW_PATH:
                mov    $0xffffffc6,%esi        ; slow path
                mov    %r11d,(%rsp)
                callq  0x00007fb38cdea1a0

看到这里的魔力?对else分支的调用内联两个编码器。请注意,即使我们将虚拟/接口调用留在支票下,这仍然有效,因为现在我们有两个具有不同类型配置文件的呼叫站点。第一个调用站点将内联为单态,类型检查将与原始内容合并instanceof。第二个呼叫站点现在实际上是双态的,并且享有双态内联。

5.4.2。C1
Benchmark (count) (p1) (p2) (p3) Mode Cnt Score Error Units

; equidistributed
Three.dynamic_Abstract_Ref 10000 1 1 1 avgt 50 138.515 ± 0.595 us/op
Three.dynamic_Interface_Ref 10000 1 1 1 avgt 50 153.387 ± 0.184 us/op
Three.static_ID_ifElse 10000 1 1 1 avgt 50 91.239 ± 0.170 us/op
Three.static_ID_switch 10000 1 1 1 avgt 50 91.631 ± 0.821 us/op

Three.peel_Abstract_Abstract 10000 1 1 1 avgt 50 146.658 ± 4.189 us/op
Three.peel_Abstract_Static 10000 1 1 1 avgt 50 136.430 ± 0.507 us/op
Three.peel_Interface_Interface 10000 1 1 1 avgt 50 156.816 ± 0.440 us/op
Three.peel_Interface_Static 10000 1 1 1 avgt 50 148.564 ± 0.541 us/op

; biased towards coder0 at 90%
Three.dynamic_Abstract_Ref 10000 18 1 1 avgt 50 72.798 ± 0.205 us/op
Three.dynamic_Interface_Ref 10000 18 1 1 avgt 50 88.188 ± 0.180 us/op
Three.static_ID_ifElse 10000 18 1 1 avgt 50 54.326 ± 0.393 us/op
Three.static_ID_switch 10000 18 1 1 avgt 50 54.454 ± 0.081 us/op

Three.peel_Abstract_Abstract 10000 18 1 1 avgt 50 83.206 ± 0.257 us/op
Three.peel_Abstract_Static 10000 18 1 1 avgt 50 65.073 ± 0.731 us/op
Three.peel_Interface_Interface 10000 18 1 1 avgt 50 87.273 ± 0.105 us/op
Three.peel_Interface_Static 10000 18 1 1 avgt 50 66.372 ± 0.121 us/op

; biased towards coder0 at 95%
Three.dynamic_Abstract_Ref 10000 38 1 1 avgt 50 67.391 ± 0.928 us/op
Three.dynamic_Interface_Ref 10000 38 1 1 avgt 50 81.523 ± 0.192 us/op
Three.static_ID_ifElse 10000 38 1 1 avgt 50 50.885 ± 0.064 us/op
Three.static_ID_switch 10000 38 1 1 avgt 50 50.374 ± 0.154 us/op

Three.peel_Abstract_Abstract 10000 38 1 1 avgt 50 76.414 ± 1.011 us/op
Three.peel_Abstract_Static 10000 38 1 1 avgt 50 58.389 ± 1.493 us/op
Three.peel_Interface_Interface 10000 38 1 1 avgt 50 81.647 ± 4.046 us/op
Three.peel_Interface_Static 10000 38 1 1 avgt 50 59.273 ± 0.848 us/op
不幸的是,这个技巧只对C1有很大帮助。在没有轮廓辅助内联的情况下,没有将类型轮廓减少到两种类型。如果他们愿意,欢迎有兴趣的读者反汇编这个案子。

5.5。讨论III
看看三个编码器的情况,我们可以发现一些事情:

即使静态分析显示有三个可能的调用目标,编译器也可以使用动态类型配置文件对推荐的接收器进行推测性优化。C2通常在单态和双态情况下执行此操作,有时在变形情况下,当有明显的赢家时。C1没有内联变形调用。

(缺少)变形内联至少损害了纳米基准。似乎有三种方法可以避免它,或者通过手动检查剥离最热的类型,或者减少TypeProfileMajorReceiverPercent让VM计算并内联最常见的目标,或者对已知目标进行静态分派。处理变形内联的处理需要在VM端以一种方式或另一种方式解决,因为这可能会影响许多现代工作负载。

结论
除非你真的需要,否则不要试图优化低级别的东西。
性能建议可能对一般性能,可维护性和SANITY有危险。它也被称为有一个非常有限的保质期,并且在没有使用时会引起头痛。在使用之前,请寻求你的性能工程师的批准。
你通常不应该担心方法分派性能。为最热门方法生成最终代码的优化编译器能够内联大多数虚拟调用。如果你将问题确定为方法分派/内联性能问题,则其余点有效。

你应该真正关心目标方法的内联性,例如它的大小,修饰符等。在这篇文章中,我们忽略了这个方面,因为我们使用的是微小的方法。但是,如果目标方法无法成功进行虚拟化,则不会进行内联。内联实际上扩大了其他优化的范围,在许多情况下,仅在内联就足够了。使用非常薄的纳米标记,这种优势无法得到适当的量化。

类层次结构分析能够静态地确定给定类只有一个子类,即使没有配置文件信息也是如此。在向层次结构中添加更多子类时要小心:CHA可能会失败,并使先前的性能假设无效。

当CHA失败时,虚拟和接口调用的C1内联也会失败,因为类型配置文件不可用。

即使CHA失败,C2也会经常内联单态和双态调用。Morphicity源自解释器或C1收集的JVM类型配置文件。

变形调用非常糟糕,C1和C2都无法内联。此时,似乎有三种方法可以避免它,或者通过手动检查剥离最热的类型,或者减少TypeProfileMajorReceiverPercent让VM计算并内联最常见的目标,或者对已知目标进行静态分派。VM可能会在将来为这些案例做得更好。

如果你有一个不依赖于实例状态的简单方法实现,那么static出于可维护性和性能原因,最好这样做 。只是为了使虚拟调用关闭而继承类实例会使JVM更难。

当你需要方法分派的最高性能时,你可以选择手动分配static实施。这违背了通常的开发实践,但是相当多的低级别性能黑客采取相同的转移路线。这就是我们在字符串压缩工作中要做的事情,特别String 是已经给出了final,并且添加引用字段String对于足迹来说比添加byteID 更糟糕。

如果你在接口和抽象类之间进行选择,则接口不应该是你的选择。 未经优化的接口调用是一种负担。但是,如果你不得不关心这种差异,那就意味着基于配置文件的去虚拟化和内联都没有发生,你可能已经搞砸了。

错误预测的分支是杀手。如果你想严格量化像这样的低级别黑客的影响,你必须考虑尝试不同的源数据混合来利用不同的分支预测行为,或者悲观地选择最不利的分支预测行为。

Comments
Write a Comment