ByteArrayOutputStream 发生了什么样的 JVM 优化?
What kind of JVM optimization happens for ByteArrayOutputStream?
我有以下 JMH 基准 (Java8):
@Benchmark
public byte[] outputStream() {
final ByteArrayOutputStream baos = new ByteArrayOutputStream();
for (int i = 0; i < size; i++) {
baos.write(i);
}
return baos.toByteArray();
}
当例如size == 65
输出如下:
# Warmup Iteration 1: 3296444.108 ops/s
# Warmup Iteration 2: 2861235.712 ops/s
# Warmup Iteration 3: 4909462.444 ops/s
# Warmup Iteration 4: 4969418.622 ops/s
# Warmup Iteration 5: 5009353.033 ops/s
Iteration 1: 5006466.075 ops/sm 19s]
...
显然,在热身 #2 期间发生了一些事情,因此之后有一个巨大的加速。
我如何确定此时发生了何种 JVM 优化?
让我们假设您在 5M ops/s 处获得了稳定的结果,这可信吗?
为了争论起见,让我们假设一个 3GHz CPU(你可能在笔记本电脑上打开了频率缩放和涡轮增压,但无论如何),5M ops/s => 每个操作 200ns => 600 个周期。我们要求 CPU 做什么?
- 分配
ByteArrayOutputStream
,默认构造函数->新字节[32],+更改
- 简单计数循环,65次,向数组写入一个字节
- 调整字节数组大小,2 次。 32 -> 64 -> 128
- 复制到新数组 (65) 和 return
- JMH 的简单循环
我们希望发生什么样的优化?
- 从解释器到本机编译(废话)
- 循环展开和大量循环优化,所有这些可能都没有多大帮助
ByteArrayOutputStream
及其众多伙伴的逃脱分析。我不认为它发生了。
我怎么知道发生了什么?我会 运行 它与一些有用的分析器。 JMH 提供了很多这些。
有了-prof gc
我可以在这里看到分配率是多少:
·gc.alloc.rate.norm: 360.000 B/op
所以,我猜 32 + 64 + 128 + 65 + change = 289b + change
=> change = 71b
,这是零钱,对吧?好吧,如果你考虑 object headers 就不会了。我们有 4 个数组和一个 object => 5 * 12 (compressed oops headers) = 60,数组长度 + ‘ByteArrayOutputStream’ 上的计数字段 = 20。所以更改应该是 80b,由我计算,但我可能遗漏了一些东西。底线是,我们没有 EscapeAnalysis。但是一些 CompressedOops 有帮助。您可以使用分配分析器,例如 JVisualVM 中的分析器来跟踪此处所有不同的分配,或者使用抽样分配分析器,例如 Java Mission Control 中的分析器。
您可以使用 -prof perfasm
查看该级别的程序集输出和配置文件。这是一个很长的练习,所以我不打算在这里进行。您可以看到其中一项很酷的优化是 JVM 不会将它在您的方法结束时创建的新数组副本清零。也可以看到,数组的分配和复制也是很耗时的地方。
最后,这里进行的明显优化只是 JIT 编译。您可以使用 JITWatch 等工具探索每个编译级别的作用。您可以使用命令行标志来查找每个编译级别的性能(-jvmArgs=-Xint' to run in the interpreter
-XX:TieredStopAtLevel=1` 停止在 C1)。
另一个正在发挥作用的大规模优化是扩大堆以适应分配率。您可以试验堆大小以了解这对性能有何影响。
玩得开心:-)
我有以下 JMH 基准 (Java8):
@Benchmark
public byte[] outputStream() {
final ByteArrayOutputStream baos = new ByteArrayOutputStream();
for (int i = 0; i < size; i++) {
baos.write(i);
}
return baos.toByteArray();
}
当例如size == 65
输出如下:
# Warmup Iteration 1: 3296444.108 ops/s
# Warmup Iteration 2: 2861235.712 ops/s
# Warmup Iteration 3: 4909462.444 ops/s
# Warmup Iteration 4: 4969418.622 ops/s
# Warmup Iteration 5: 5009353.033 ops/s
Iteration 1: 5006466.075 ops/sm 19s]
...
显然,在热身 #2 期间发生了一些事情,因此之后有一个巨大的加速。
我如何确定此时发生了何种 JVM 优化?
让我们假设您在 5M ops/s 处获得了稳定的结果,这可信吗? 为了争论起见,让我们假设一个 3GHz CPU(你可能在笔记本电脑上打开了频率缩放和涡轮增压,但无论如何),5M ops/s => 每个操作 200ns => 600 个周期。我们要求 CPU 做什么?
- 分配
ByteArrayOutputStream
,默认构造函数->新字节[32],+更改 - 简单计数循环,65次,向数组写入一个字节
- 调整字节数组大小,2 次。 32 -> 64 -> 128
- 复制到新数组 (65) 和 return
- JMH 的简单循环
我们希望发生什么样的优化?
- 从解释器到本机编译(废话)
- 循环展开和大量循环优化,所有这些可能都没有多大帮助
ByteArrayOutputStream
及其众多伙伴的逃脱分析。我不认为它发生了。
我怎么知道发生了什么?我会 运行 它与一些有用的分析器。 JMH 提供了很多这些。
有了-prof gc
我可以在这里看到分配率是多少:
·gc.alloc.rate.norm: 360.000 B/op
所以,我猜 32 + 64 + 128 + 65 + change = 289b + change
=> change = 71b
,这是零钱,对吧?好吧,如果你考虑 object headers 就不会了。我们有 4 个数组和一个 object => 5 * 12 (compressed oops headers) = 60,数组长度 + ‘ByteArrayOutputStream’ 上的计数字段 = 20。所以更改应该是 80b,由我计算,但我可能遗漏了一些东西。底线是,我们没有 EscapeAnalysis。但是一些 CompressedOops 有帮助。您可以使用分配分析器,例如 JVisualVM 中的分析器来跟踪此处所有不同的分配,或者使用抽样分配分析器,例如 Java Mission Control 中的分析器。
您可以使用 -prof perfasm
查看该级别的程序集输出和配置文件。这是一个很长的练习,所以我不打算在这里进行。您可以看到其中一项很酷的优化是 JVM 不会将它在您的方法结束时创建的新数组副本清零。也可以看到,数组的分配和复制也是很耗时的地方。
最后,这里进行的明显优化只是 JIT 编译。您可以使用 JITWatch 等工具探索每个编译级别的作用。您可以使用命令行标志来查找每个编译级别的性能(-jvmArgs=-Xint' to run in the interpreter
-XX:TieredStopAtLevel=1` 停止在 C1)。
另一个正在发挥作用的大规模优化是扩大堆以适应分配率。您可以试验堆大小以了解这对性能有何影响。
玩得开心:-)