JEP 285: Spin-Wait Hints | 自旋等待提示
摘要
定义一个 API,允许 Java 代码提示正在执行自旋循环。
目标
定义一个 API,允许 Java 代码向运行时系统提示它处于自旋循环中。 该 API 将是纯粹的提示,不会带来任何语义行为要求(例如,无操作是有效的实现)。允许 JVM 从可能在某些硬件平台上有用的自旋循环特定行为中受益。在 JDK 中提供一个无操作实现和一个内部实现,并在至少一个主要硬件平台上展示执行效益。
非目标
本 JEP 不考虑除自旋循环之外的性能提示。其他性能提示,如预取提示,超出了本 JEP 的范围。
动机
一些硬件平台从软件表示正在进行的自旋循环中受益。可以观察到一些常见的执行效益:
- 使用自旋提示时,自旋循环的响应时间可能会改善,这是由于各种因素,减少了线程之间的延迟等待情况;
- 参与自旋循环的核心或硬件线程消耗的功率可能会降低,从而有利于程序的整体功耗,并可能允许其他核心或硬件线程以相同功耗范围内的更快速度执行。
虽然长期自旋通常不被鼓励作为一般用户模式编程实践,但短期自旋在阻塞之前是一种常见实践(无论是在 JDK 内部还是外部)。此外,由于多核计算平台普遍可用,许多性能和延迟敏感的应用程序(如 Disruptor)使用一种将自旋线程专用于延迟关键功能并可能包含长时间自旋的模式。
作为一个实际的例子和用例,当前的 x86 处理器支持使用 PAUSE
指令来指示自旋行为。使用 PAUSE
指令可以明显减少线程之间的往返延迟。由于其好处和广泛推荐的使用,x86 的 PAUSE
指令通常用于内核自旋锁,在执行启发式自旋之前的 POSIX 库中,甚至由 JVM 本身使用。然而,由于无法提示 Java 循环正在自旋,它的好处对于常规 Java 代码是不可用的。
我们提供了具体的支持证据:在 E5-2697 v2 上进行的简单测试中,测量两个通过在易失性字段上自旋进行通信的线程之间的往返延迟行为时,往返延迟在广泛的百分位数范围(从 10%ile 到 99.9%ile)明显减少了 18-20 纳秒。例如,当两个自旋线程在共享物理 CPU 核心和 L1 数据缓存的两个硬件线程上执行时,这种减少可以表示最佳情况下线程间通信延迟的 35%-50% 的改进。测试的完整列表可以在 这里 找到。
上图显示了一个示例延迟测量,比较包含内部的 spinLoopHint()
调用(作为 PAUSE
指令内联)的自旋循环的反应延迟与不使用 PAUSE
指令执行的相同循环,以及执行实际的 System.nanoTime()
调用测量时间所需的时间。
描述
我们建议向 JDK 添加一个方法来提示正在执行自旋循环:java.lang.Thread.onSpinWait()
。
一个空方法将是 java.lang.Thread.onSpinWait()
方法的有效实现,但内部实现是对可以受益于它的硬件平台的明显目标。我们打算作为此 JEP 的一部分,在 JDK 中生成一个内部的 x86 实现。原型实现已经存在,初步测试结果表明有希望。请参考 JBS bug JDK-8147844,了解类库和 JVM 中建议的更改的 webrev。
备选方案
可以使用 JNI 来循环使用自旋循环提示的 CPU 指令,但 JNI 边界交叉开销往往大于该指令提供的好处,至少在涉及延迟的情况下。
我们可以尝试让 JIT 编译器推断自旋循环情况,并自动包含自旋循环提示的 CPU 指令,无需 Java 代码提示。我们怀疑自动和可靠地检测自旋情况的复杂性,以及在某些平台上使用提示的潜在权衡的问题,将严重延迟可行实现的可用性。
测试
对于 "vanilla" 无操作实现的测试显然非常简单。
我们相信,鉴于此 API 的非常小的占用空间,测试内部化的 x86 实现也将是直截了当的。我们期望测试集中于确认代码生成的正确性和使用内部化实现时使用自旋循环提示的延迟优势。
如果将此 API 作为 Java SE API(例如,用于将来的 Java SE 9 或 Java SE 10 中的 java.*
命名空间)接受,我们期望为 API 开发相关的 TCK 测试,以便潜在地包含在 Java SE TCK 中。
风险和假设
"vanilla" 无操作实现显然是相当低风险的。内部的 x86 实现将涉及对多个 JVM 组件的修改,因此它们具有一些风险,但不比其他简单的内部化添加到 JDK 中的功能更多。