JVM 解剖公园(18): 标量替换
Posted ImportNew
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了JVM 解剖公园(18): 标量替换相关的知识,希望对你有一定的参考价值。
(给ImportNew加星标,提高Java技能)
编译:ImportNew/唐尤华
shipilev.net/jvm/anatomy-quarks/18-scalar-replacement/
1. 写在前面
“JVM 解剖公园”是一个持续更新的系列迷你博客,阅读每篇文章一般需要5到10分钟。限于篇幅,仅对某个主题按照问题、测试、基准程序、观察结果深入讲解。因此,这里的数据和讨论可以当轶事看,不做写作风格、句法和语义错误、重复或一致性检查。如果选择采信文中内容,风险自负。
Aleksey Shipilёv,JVM 性能极客
推特 @shipilev
问题、评论、建议发送到 aleksey@shipilev.net"">aleksey@shipilev.net
2. 问题
据说 Hotspot 可以进行堆栈分配,称作逃逸分析(Escape Analysis 简称 EA)。很神奇,对吧?
3. 理论
这种说法令人费解。似乎在“堆栈分配”中,“分配”操作假定整个对象是在堆栈上分配,而不在堆上分配。但实际情况是,编译器会先进行逃逸分析,识别没有逃逸到堆中的新建对象,然后开始做一些有趣的优化。注意:逃逸分析本身不做优化,而是为优化器提供重要的分析数据。
(1) 有人声称逃逸分析实际进行了优化,这让我有些恼火。逃逸分析不做优化,接下来才真正开始优化!
优化器对没有逃逸的对象会重新映射,把对象字段访问映射为访问合成后的局部操作数:进行标量替换(Scalar Replacement)。接下来这些操作数会交给寄存器分配器(register allocator)处理,由于其中一些可能会在当前活跃方法中申请 stack slot(所谓“溢出”),对象字段内存块看起来好像是在堆栈上分配。但是这种映射可能出错且无法对等:操作数可能根本没有出现,也可能驻留在寄存器中,甚至没有创建对象头。与堆栈分配不同,对象字段访问映射的操作数在堆栈上可能不连续。
(2) 类似于编译器中局部变量与其它临时操作数的中间形式。
如果堆栈实际完成分配,将在堆栈上存储包括对象头和字段在内的整个对象,并在生成的代码中引用。在这个方案中需要注意:由于不能确保当前线程一直执行指定方法且持有的对象保持活跃,一旦对象发生逃逸,需要将整个对象块从堆栈复制到堆中。这意味着必须拦截堆存储操作,即以在存储堆栈对象时启动 GC 写屏障。
Hotspot 本身不进行堆栈分配,但使用标量替换完成近似操作。
在实验中能观察到上述结论吗?
4. 实验
看下面这个 JMH 基准测试:创建只有一个字段的对象,初始化并作为输入。接着立刻读取对象字段,然后抛弃对象:
import org.openjdk.jmh.annotations.*;
@Warmup(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS)
@Measurement(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS)
@Fork(3)
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
@State(Scope.Benchmark)
public class ScalarReplacement {
int x;
@Benchmark
public int single() {
MyObject o = new MyObject(x);
return o.x;
}
static class MyObject {
final int x;
public MyObject(int x) {
this.x = x;
}
}
}
用 -prof gc 运行测试,会注意到实际上并没有执行分配:
Benchmark Mode Cnt Score Error Units
ScalarReplacement.single avgt 15 1.919 ± 0.002 ns/op
ScalarReplacement.single:·gc.alloc.rate avgt 15 ≈ 10⁻⁴ MB/sec
ScalarReplacement.single:·gc.alloc.rate.norm avgt 15 ≈ 10⁻⁶ B/op
ScalarReplacement.single:·gc.count avgt 15 ≈ 0 counts
-prof perfasm 的结果表明,字段 x 只访问1次。
....[Hottest Region 1].............................................................
C2, level 4, org.openjdk.ScalarReplacement::single, version 459 (26 bytes)
[Verified Entry Point]
6.05% 2.82% 0x00007f79e1202900: sub $0x18,%rsp ; prolog
0.95% 0.78% 0x00007f79e1202907: mov %rbp,0x10(%rsp)
0.04% 0.21% 0x00007f79e120290c: mov 0xc(%rsi),%eax ; 获取字段 $x
5.80% 7.43% 0x00007f79e120290f: add $0x10,%rsp ; epilog
0x00007f79e1202913: pop %rbp
23.91% 33.34% 0x00007f79e1202914: test %eax,0x17f0b6e6(%rip)
0.21% 0.02% 0x00007f79e120291a: retq
...................................................................................
当然,识别不会发生逃逸的对象需要进行复杂的逃逸分析。逃逸分析中断时,标量替换也会中断。打断 Hotspot 逃逸分析最简单的办法是在访问字段前合并控制流。例如,有两个不同对象(但内容相同),选择其中任何一个分支都会中断逃逸分析。尽管对人类而言这两个对象显然不会发生逃逸:
public class ScalarReplacement {
int x;
boolean flag;
@Setup(Level.Iteration)
public void shake() {
flag = ThreadLocalRandom.current().nextBoolean();
}
@Benchmark
public int split() {
MyObject o;
if (flag) {
o = new MyObject(x);
} else {
o = new MyObject(x);
}
return o.x;
}
// ...
}
执行代码后分配内存的结果:
Benchmark Mode Cnt Score Error Units
ScalarReplacement.single avgt 15 1.919 ± 0.002 ns/op
ScalarReplacement.single:·gc.alloc.rate avgt 15 ≈ 10⁻⁴ MB/sec
ScalarReplacement.single:·gc.alloc.rate.norm avgt 15 ≈ 10⁻⁶ B/op
ScalarReplacement.single:·gc.count avgt 15 ≈ 0 counts
ScalarReplacement.split avgt 15 3.781 ± 0.116 ns/op
ScalarReplacement.split:·gc.alloc.rate avgt 15 2691.543 ± 81.183 MB/sec
ScalarReplacement.split:·gc.alloc.rate.norm avgt 15 16.000 ± 0.001 B/op
ScalarReplacement.split:·gc.count avgt 15 1460.000 counts
ScalarReplacement.split:·gc.time avgt 15 929.000 ms
如果这是一次“真正的”堆栈分配,处理这种情况很容易:每次分配首先增加堆栈空间,接着执行访问,最后在离开方法前清空堆栈内容并收回堆栈分配内存。但是防止对象逃逸带来的写屏障问题仍然存在。
5. 观察
逃逸分析是一种有趣的编译器优化技术,可用来优化。标量替换是其中一种。但逃逸分析并没有把对象存储到堆栈上,相反,它会拆分对象并把代码重写为本地访问,实现进一步优化。当寄存器访问压力过高时,会访问堆栈。在许多关键的 hotpath 上,逃逸分析能够成功地完成性能优化。
逃逸分析并不完美:如果不能通过静态分析确认对象不会逃逸,则必须假设会发生逃逸。复杂的控制流程可能会提前退出。调用非内联实例方法也会结束逃逸分析,因为对于当前分析不透明。尽管一些琐碎的情况能够得到有效处理,比如与非逃逸对象比较引,但是与对象身份相关的事情还是会导致逃逸分析结束。
虽然并不完美,但当它起作用时表现非常出色。编译器技术的进步可能会增加逃逸分析成功案例。
(3) 例如,Graal 就以局部逃逸分析特性著称,在复杂的数据流情况下具有更好的适应性。
推荐阅读
(点击标题可跳转阅读)
看完本文有收获?请转发分享给更多人
关注「ImportNew」,提升Java技能
好文章,我在看❤️
以上是关于JVM 解剖公园(18): 标量替换的主要内容,如果未能解决你的问题,请参考以下文章