在简单情况下,例如 for-each 循环中的迭代器,Hotspot 中的转义分析有多脆弱

Mar*_* VY 5 java jit jvm jvm-hotspot escape-analysis

假设我有一个 java.util.Collection 想要循环。通常我会这样做:

for(Thing thing : things) do_something_with(thing);
Run Code Online (Sandbox Code Playgroud)

但是假设这是在一些到处使用的核心实用方法中,并且在大多数地方,集合是空的。那么理想情况下,我们不希望仅仅为了执行无操作循环而对每个调用者强加迭代器分配,并且我们可以重写如下内容:

if(things.isEmpty()) return;
for(Thing thing : things) do_something_with(thing);
Run Code Online (Sandbox Code Playgroud)

如果是列表,则更极端的选择things是使用 C 样式for循环。

但是等等,Java 转义分析应该消除这种分配,至少在 C2 编译器处理此方法之后。所以应该不需要这种“纳米优化”。(我什至不会用微优化这个词来形容它;它对于这个来说有点太小了。)除了......

我一直听说逃逸分析是“脆弱的”,但似乎没有人谈论过什么会导致它变得混乱。直觉上,我认为更复杂的控制流将是最值得担心的,这意味着 for-each 循环中的迭代器应该被可靠地消除,因为那里的控制流很简单。

这里的标准响应是尝试进行实验,但除非我知道其中的变量,否则很难相信我可能想从这样的实验中得出的任何结论。

事实上,在这篇博客文章中,有人尝试了这样的实验,三分之二的分析器给出了错误的结果:

http://psy-lob-saw.blogspot.com/2014/12/the-escape-of-arraylistiterator.html

我对晦涩难懂的 JVM 魔法的了解比该博客文章的作者要少得多,而且很可能更容易被误导。

apa*_*gin 4

标量替换确实是一种你永远无法绝对确定的优化,因为它取决于太多因素。

\n

首先,只有当实例的所有使用都内联在一个编译单元中时,才可以消除分配。对于迭代器,这意味着迭代器构造函数hasNextnext调用(包括嵌套调用)必须内联。

\n
public E next() {\n    if (! hasNext())\n        throw new NoSuchElementException();\n    return (E) snapshot[cursor++];\n}\n
Run Code Online (Sandbox Code Playgroud)\n

然而,内联本身在 HotSpot 中是一种脆弱的优化,因为它依赖于许多启发式方法和限制。例如,iterator.next()由于达到最大内联深度,或者因为外部编译已经太大,可能会发生调用未完全内联到循环中的情况。

\n

其次,如果引用有条件地接收不同的值,则不会发生标量替换。

\n
for(Thing thing : things) do_something_with(thing);\n
Run Code Online (Sandbox Code Playgroud)\n

在您的示例中, if thingsis 有时ArrayList和 有时Collections.emptyList(),迭代器将在堆上分配。为了进行消除,迭代器的类型必须始终相同。

\n

Ruslan Cheremin 的关于标量替换的精彩演讲中还有更多示例(该演讲是俄语的,但 YouTube 的字幕翻译功能可以解决这个问题)。

\n

另一篇推荐阅读的文章是 Aleksey Shipil\xd1\x91v\ 的博客文章,其中还演示了如何使用JMH来验证标量替换是否在特定场景中发生。

\n

简而言之,在像您这样的简单情况下,分配消除很可能会按预期工作。但正如我上面提到的,可能存在一些边缘情况。

\n

最近在邮件列表上有一个hotspot-compiler-dev关于部分逃逸分析提案的讨论。如果实施,它可以显着扩展标量替换优化的适用性。

\n