Почему "массив" прямой памяти медленнее очищается, чем обычный массив Java?

Я установил тест JMH, чтобы измерить, что будет быстрее Arrays.fill с нулем, System.arraycopy из нулевого массива, обнуление DirectByteBuffer или обнуление unsafe Блок памяти пытается ответить на этот вопрос. Давайте отложим в сторону, что обнуление непосредственно выделенной памяти - редкий случай, и обсудим результаты моего теста.

Вот фрагмент кода JMH ( полный код доступен через gist), включая unsafe.setMemory случай, как предложено @apangin в оригинальном сообщении, byteBuffer.put(byte[], offset, length) а также longBuffer.put(long[], offset, length) как предложено @jan-schaefer:

@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 ГГц, 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?

2 ответа

Решение

Почему unsafeArraySetMemory немного, но медленнее, чем unsafeArrayManualLoop?

Я думаю, что он не так хорошо оптимизирован для установки ровно нескольких длинных. Он должен проверить, есть ли у вас что-то, не очень кратное 8.

Почему directByteBuffer на порядок медленнее, чем другие?

Порядок величины будет примерно в 10 раз, это примерно в 2,5 раза медленнее. Он должен ограничивать проверку каждого доступа и обновлять поле вместо локальной переменной.

ПРИМЕЧАНИЕ. Я обнаружил, что JVM не всегда зацикливает код развертывания с помощью Unsafe. Вы можете попробовать сделать это самостоятельно, чтобы увидеть, поможет ли это.

ПРИМЕЧАНИЕ. Нативный код может использовать 128-битные инструкции XMM и использует его все чаще, поэтому копирование может быть таким быстрым. Доступ к инструкции XMM может прийти в Java 10.

Сравнение немного несправедливо. Вы используете одну операцию при использовании Array.fill а также System.arraycopy, но вы используете цикл и несколько вызовов putLong в DirectByteBuffer дело. Если вы посмотрите на реализацию putLong вы увидите, что там много чего происходит, например, проверка доступности. Вы должны попытаться использовать пакетную операцию, как put(long[] src, int srcOffset, int longCount) и посмотрим, что получится.

Другие вопросы по тегам