装箱与创建 1 个元素的数组
Boxing vs creating array of 1 element
免责声明:我阅读了 Alexey Shipilev 的 this article 并了解到纳米基准测试是一种邪恶。但无论如何都想自己试验和理解。
我正在尝试衡量 byte
的数组创建与装箱。这是我的基准:
@Fork(1)
@Warmup(iterations = 5, timeUnit = TimeUnit.NANOSECONDS)
@Measurement(iterations = 5, timeUnit = TimeUnit.NANOSECONDS)
public class MyBenchmark {
@Benchmark
@OutputTimeUnit(TimeUnit.NANOSECONDS)
@BenchmarkMode(Mode.AverageTime)
public void arrayBenchmark(Blackhole bh) {
byte[] b = new byte[1];
b[0] = 20;
bh.consume(b);
}
@Benchmark
@OutputTimeUnit(TimeUnit.NANOSECONDS)
@BenchmarkMode(Mode.AverageTime)
public void bonxingBenchmark(Blackhole bh) {
bh.consume(new Byte((byte) 20));
}
}
我 运行 这个基准测试了好几次,出于某种原因,我发现装箱比创建数组并将元素放入其中快 1.5 倍
所以我决定 运行 这个基准与 -prof gc
。结果是这样的:
MyBenchmark.arrayBenchmark avgt 5 7.751 ± 0.537 ns/op
MyBenchmark.arrayBenchmark:·gc.alloc.rate avgt 5 1966.743 ± 143.624 MB/sec
MyBenchmark.arrayBenchmark:·gc.alloc.rate.norm avgt 5 24.000 ± 0.001 B/op
MyBenchmark.arrayBenchmark:·gc.churn.PS_Eden_Space avgt 5 1966.231 ± 326.378 MB/sec
MyBenchmark.arrayBenchmark:·gc.churn.PS_Eden_Space.norm avgt 5 23.999 ± 4.148 B/op
MyBenchmark.arrayBenchmark:·gc.churn.PS_Survivor_Space avgt 5 0.042 ± 0.113 MB/sec
MyBenchmark.arrayBenchmark:·gc.churn.PS_Survivor_Space.norm avgt 5 0.001 ± 0.001 B/op
MyBenchmark.arrayBenchmark:·gc.count avgt 5 37.000 counts
MyBenchmark.arrayBenchmark:·gc.time avgt 5 48.000 ms
MyBenchmark.bonxingBenchmark avgt 5 6.123 ± 1.306 ns/op
MyBenchmark.bonxingBenchmark:·gc.alloc.rate avgt 5 1664.504 ± 370.508 MB/sec
MyBenchmark.bonxingBenchmark:·gc.alloc.rate.norm avgt 5 16.000 ± 0.001 B/op
MyBenchmark.bonxingBenchmark:·gc.churn.PS_Eden_Space avgt 5 1644.547 ± 1004.476 MB/sec
MyBenchmark.bonxingBenchmark:·gc.churn.PS_Eden_Space.norm avgt 5 15.769 ± 7.495 B/op
MyBenchmark.bonxingBenchmark:·gc.churn.PS_Survivor_Space avgt 5 0.037 ± 0.067 MB/sec
MyBenchmark.bonxingBenchmark:·gc.churn.PS_Survivor_Space.norm avgt 5 ≈ 10⁻³ B/op
MyBenchmark.bonxingBenchmark:·gc.count avgt 5 23.000 counts
MyBenchmark.bonxingBenchmark:·gc.time avgt 5 37.000 ms
正如我们所见,GC
在 arrayBenchmark
情况下负载很重。分配率 1966
对比 1664
。 gc-count
和 gc-time
也不同。 我认为是这个原因,但不确定
目前我不太理解这种行为。我认为在我的例子中数组分配只是意味着我们在某处分配 1 个字节。对我来说,它看起来与 Boxing
几乎相同,但实际上不同。
你能帮我理解一下吗?
最重要的是...我可以相信这个基准吗?
TL;DR 这是因为 Java objects memory layout* 和数组开销。
每个object都有header(存储身份哈希码、锁和gc meta-information)和class指针。此外,object 的地址应与 8.
对齐
假设您使用的是 x86-64 处理器,header 大小为 8 字节,class 指针大小为 4 字节。
Byte
中的 byte
字段占用 1 个字节 => Byte
应该占用 13 个字节,但是四舍五入到对齐正好给你 16 个字节,你用 -prof gc
.[=22 观察到了这一点=]
对于数组的计算大部分是一样的,但是数组有4字节的length
字段(从技术上讲,它不是一个真正的字段,但它并不重要),这给你8 + 4 + 4 = 16
字节为数组,1 个字节为单字节元素,对齐后为您提供 24 个字节。
所以原因之一是数组更大 object。
第二个原因是创建数组的代码更复杂(您可以使用 -prof perfasm
或 -prof dtraceasm
查看生成的代码)。数组初始化需要额外存储到 length
字段,而且(我不知道为什么)JIT 生成四个 prefetchnta
指令。
所以是的,你可以部分相信它:分配单个元素稍微(虽然不是 x1.5 倍)慢。
*请注意,所有这些仅与 Hotspot 相关,其他 JVM 可能有不同的布局。
免责声明:我阅读了 Alexey Shipilev 的 this article 并了解到纳米基准测试是一种邪恶。但无论如何都想自己试验和理解。
我正在尝试衡量 byte
的数组创建与装箱。这是我的基准:
@Fork(1)
@Warmup(iterations = 5, timeUnit = TimeUnit.NANOSECONDS)
@Measurement(iterations = 5, timeUnit = TimeUnit.NANOSECONDS)
public class MyBenchmark {
@Benchmark
@OutputTimeUnit(TimeUnit.NANOSECONDS)
@BenchmarkMode(Mode.AverageTime)
public void arrayBenchmark(Blackhole bh) {
byte[] b = new byte[1];
b[0] = 20;
bh.consume(b);
}
@Benchmark
@OutputTimeUnit(TimeUnit.NANOSECONDS)
@BenchmarkMode(Mode.AverageTime)
public void bonxingBenchmark(Blackhole bh) {
bh.consume(new Byte((byte) 20));
}
}
我 运行 这个基准测试了好几次,出于某种原因,我发现装箱比创建数组并将元素放入其中快 1.5 倍
所以我决定 运行 这个基准与 -prof gc
。结果是这样的:
MyBenchmark.arrayBenchmark avgt 5 7.751 ± 0.537 ns/op
MyBenchmark.arrayBenchmark:·gc.alloc.rate avgt 5 1966.743 ± 143.624 MB/sec
MyBenchmark.arrayBenchmark:·gc.alloc.rate.norm avgt 5 24.000 ± 0.001 B/op
MyBenchmark.arrayBenchmark:·gc.churn.PS_Eden_Space avgt 5 1966.231 ± 326.378 MB/sec
MyBenchmark.arrayBenchmark:·gc.churn.PS_Eden_Space.norm avgt 5 23.999 ± 4.148 B/op
MyBenchmark.arrayBenchmark:·gc.churn.PS_Survivor_Space avgt 5 0.042 ± 0.113 MB/sec
MyBenchmark.arrayBenchmark:·gc.churn.PS_Survivor_Space.norm avgt 5 0.001 ± 0.001 B/op
MyBenchmark.arrayBenchmark:·gc.count avgt 5 37.000 counts
MyBenchmark.arrayBenchmark:·gc.time avgt 5 48.000 ms
MyBenchmark.bonxingBenchmark avgt 5 6.123 ± 1.306 ns/op
MyBenchmark.bonxingBenchmark:·gc.alloc.rate avgt 5 1664.504 ± 370.508 MB/sec
MyBenchmark.bonxingBenchmark:·gc.alloc.rate.norm avgt 5 16.000 ± 0.001 B/op
MyBenchmark.bonxingBenchmark:·gc.churn.PS_Eden_Space avgt 5 1644.547 ± 1004.476 MB/sec
MyBenchmark.bonxingBenchmark:·gc.churn.PS_Eden_Space.norm avgt 5 15.769 ± 7.495 B/op
MyBenchmark.bonxingBenchmark:·gc.churn.PS_Survivor_Space avgt 5 0.037 ± 0.067 MB/sec
MyBenchmark.bonxingBenchmark:·gc.churn.PS_Survivor_Space.norm avgt 5 ≈ 10⁻³ B/op
MyBenchmark.bonxingBenchmark:·gc.count avgt 5 23.000 counts
MyBenchmark.bonxingBenchmark:·gc.time avgt 5 37.000 ms
正如我们所见,GC
在 arrayBenchmark
情况下负载很重。分配率 1966
对比 1664
。 gc-count
和 gc-time
也不同。 我认为是这个原因,但不确定
目前我不太理解这种行为。我认为在我的例子中数组分配只是意味着我们在某处分配 1 个字节。对我来说,它看起来与 Boxing
几乎相同,但实际上不同。
你能帮我理解一下吗?
最重要的是...我可以相信这个基准吗?
TL;DR 这是因为 Java objects memory layout* 和数组开销。
每个object都有header(存储身份哈希码、锁和gc meta-information)和class指针。此外,object 的地址应与 8.
对齐假设您使用的是 x86-64 处理器,header 大小为 8 字节,class 指针大小为 4 字节。
Byte
中的 byte
字段占用 1 个字节 => Byte
应该占用 13 个字节,但是四舍五入到对齐正好给你 16 个字节,你用 -prof gc
.[=22 观察到了这一点=]
对于数组的计算大部分是一样的,但是数组有4字节的length
字段(从技术上讲,它不是一个真正的字段,但它并不重要),这给你8 + 4 + 4 = 16
字节为数组,1 个字节为单字节元素,对齐后为您提供 24 个字节。
所以原因之一是数组更大 object。
第二个原因是创建数组的代码更复杂(您可以使用 -prof perfasm
或 -prof dtraceasm
查看生成的代码)。数组初始化需要额外存储到 length
字段,而且(我不知道为什么)JIT 生成四个 prefetchnta
指令。
所以是的,你可以部分相信它:分配单个元素稍微(虽然不是 x1.5 倍)慢。
*请注意,所有这些仅与 Hotspot 相关,其他 JVM 可能有不同的布局。