装箱与创建 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

正如我们所见,GCarrayBenchmark 情况下负载很重。分配率 1966 对比 1664gc-countgc-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 可能有不同的布局。