为啥直接内存“数组”的清除速度比通常的 Java 数组慢?

Posted

技术标签:

【中文标题】为啥直接内存“数组”的清除速度比通常的 Java 数组慢?【英文标题】:Why direct memory 'array' is slower to clear than a usual Java array?为什么直接内存“数组”的清除速度比通常的 Java 数组慢? 【发布时间】:2017-02-17 13:27:58 【问题描述】:

我已经设置了一个 JMH 基准测试来测量什么会更快 Arrays.fill 与 null、System.arraycopy 来自一个空数组、将 DirectByteBuffer 归零或将 unsafe 内存块归零试图回答这个question 让我们先将直接分配的内存归零这种情况很少见,并讨论一下我的基准测试的结果。

这是 JMH 基准 sn-p (full code available via a gist),包括 @apangin 在原始帖子中建议的 unsafe.setMemory 案例,@jan-schaefer 建议的 byteBuffer.put(byte[], offset, length)longBuffer.put(long[], offset, length)

@Benchmark
@BenchmarkMode(Mode.SampleTime)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
public void arrayFill() 
    Arrays.fill(objectHolderForFill, null);


@Benchmark
@BenchmarkMode(Mode.SampleTime)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
public void arrayCopy() 
    System.arraycopy(nullsArray, 0, objectHolderForArrayCopy, 0, objectHolderForArrayCopy.length);


@Benchmark
@BenchmarkMode(Mode.SampleTime)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
public void directByteBufferManualLoop() 
    while (referenceHolderByteBuffer.hasRemaining()) 
        referenceHolderByteBuffer.putLong(0);
    


@Benchmark
@BenchmarkMode(Mode.SampleTime)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
public void directByteBufferBatch() 
    referenceHolderByteBuffer.put(nullBytes, 0, nullBytes.length);


@Benchmark
@BenchmarkMode(Mode.SampleTime)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
public void directLongBufferManualLoop() 
    while (referenceHolderLongBuffer.hasRemaining()) 
        referenceHolderLongBuffer.put(0L);
    


@Benchmark
@BenchmarkMode(Mode.SampleTime)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
public void directLongBufferBatch() 
    referenceHolderLongBuffer.put(nullLongs, 0, nullLongs.length);



@Benchmark
@BenchmarkMode(Mode.SampleTime)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
public void unsafeArrayManualLoop() 
    long addr = referenceHolderUnsafe;
    long pos = 0;
    for (int i = 0; i < size; i++) 
        unsafe.putLong(addr + pos, 0L);
        pos += 1 << 3;
    


@Benchmark
@BenchmarkMode(Mode.SampleTime)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
public void unsafeArraySetMemory() 
    unsafe.setMemory(referenceHolderUnsafe, size*8, (byte) 0);

这是我得到的(Java 1.8、JMH 1.13、Core i3-6100U 2.30 GHz、Win10):

100 elements
Benchmark                                       Mode      Cnt   Score   Error    Units
ArrayNullFillBench.arrayCopy                   sample  5234029  39,518 ± 0,991    ns/op
ArrayNullFillBench.directByteBufferBatch       sample  6271334  43,646 ± 1,523    ns/op
ArrayNullFillBench.directLongBufferBatch       sample  4615974  45,252 ± 2,352    ns/op
ArrayNullFillBench.arrayFill                   sample  4745406  76,997 ± 3,547    ns/op
ArrayNullFillBench.unsafeArrayManualLoop       sample  5980381  78,811 ± 2,870    ns/op
ArrayNullFillBench.unsafeArraySetMemory        sample  5985884  85,062 ± 2,096    ns/op
ArrayNullFillBench.directLongBufferManualLoop  sample  4697023  116,242 ± 2,579   ns/op WOW
ArrayNullFillBench.directByteBufferManualLoop  sample  7504629  208,440 ± 10,651  ns/op WOW

I skipped all the loop implementations (except arrayFill for scale) from further tests

1000 elements
Benchmark                                 Mode      Cnt    Score   Error    Units
ArrayNullFillBench.arrayCopy              sample  6780681  184,516 ± 14,036  ns/op
ArrayNullFillBench.directLongBufferBatch  sample  4018778  293,325 ± 4,074   ns/op
ArrayNullFillBench.directByteBufferBatch  sample  4063969  313,171 ± 4,861   ns/op
ArrayNullFillBench.arrayFill              sample  6862928  518,886 ± 6,372   ns/op

10000 elements
Benchmark                                 Mode      Cnt     Score   Error    Units
ArrayNullFillBench.arrayCopy              sample  2551851  2024,543 ± 12,533  ns/op
ArrayNullFillBench.directLongBufferBatch  sample  2958517  4469,210 ± 10,376  ns/op
ArrayNullFillBench.directByteBufferBatch  sample  2892258  4526,945 ± 33,443  ns/op
ArrayNullFillBench.arrayFill              sample  5689507  5028,592 ± 9,074   ns/op

您能否澄清以下问题:

1. Why `unsafeArraySetMemory` is a bit but slower than `unsafeArrayManualLoop`?
2. Why directByteBuffer is 2.5X-5X slower than others?

【问题讨论】:

【参考方案1】:

为什么 unsafeArraySetMemory 比 unsafeArrayManualLoop 有点但慢?

我的猜测是它没有为设置多个 long 进行优化。它必须检查你是否有东西,而不是 8 的倍数。

为什么 directByteBuffer 比其他的慢一个数量级?

一个数量级大约是 10 倍,大约慢 2.5 倍。它必须对每次访问进行边界检查并更新字段而不是局部变量。

注意:我发现 JVM 并不总是使用 Unsafe 循环展开代码。您可以自己尝试这样做,看看是否有帮助。

注意:本机代码可以使用 XMM 128 位指令,并且越来越多地使用它,这就是复制速度可能如此之快的原因。访问 XMM 指令可能会出现在 Java 10 中。

【讨论】:

【参考方案2】:

比较有点不公平。您在使用Array.fillSystem.arraycopy 时使用了单个操作,但在DirectByteBuffer 案例中使用了putLong 的循环和多次调用。如果您查看putLong 的实现,您会发现那里有很多事情要做,例如检查可访问性。您应该尝试使用像 put(long[] src, int srcOffset, int longCount) 这样的批处理操作,看看会发生什么。

【讨论】:

谢谢,我也会在批处理操作中添加该案例。 Array.fill 在下面使用相同的循环。 unsafe.setMemory 也是一种批处理操作。 刚刚添加了您建议的案例(ByteBufferLongBuffer 版本以防万一)。似乎即使使用 DirectBuffer 的批处理操作仍然比 System.arraycopy 慢,并且在更大的数组大小上往往更接近 Array.fill

以上是关于为啥直接内存“数组”的清除速度比通常的 Java 数组慢?的主要内容,如果未能解决你的问题,请参考以下文章

为啥直接搜索 MongoDB 分片的全文搜索比通过集群管理器 (mongos) 实例快得多?

HashMap为啥比数组查询速度快?

近期学习Java的收获

java中怎么判断arraylist占用的内存空间大小

为啥电脑刚开机时速度很快,但过一段时间速度就及慢了?

java 基础之 list