RandomAccessFile.setLength намного медленнее на Java 10 (Centos)

Следующий код

public class Main {
    public static void main(String[] args) throws IOException {
        File tmp = File.createTempFile("deleteme", "dat");
        tmp.deleteOnExit();
        RandomAccessFile raf = new RandomAccessFile(tmp, "rw");
        for (int t = 0; t < 10; t++) {
            long start = System.nanoTime();
            int count = 5000;
            for (int i = 1; i < count; i++)
                raf.setLength((i + t * count) * 4096);
            long time = System.nanoTime() - start;
            System.out.println("Average call time " + time / count / 1000 + " us.");
        }
    }
}

На Java 8 это работает нормально (файл находится на tmpfs, поэтому вы ожидаете, что это будет тривиально)

Average call time 1 us.
Average call time 0 us.
Average call time 0 us.
Average call time 0 us.
Average call time 0 us.
Average call time 0 us.
Average call time 0 us.
Average call time 0 us.
Average call time 0 us.
Average call time 0 us.

На Java 10 это становится все медленнее, когда файл становится больше

Average call time 311 us.
Average call time 856 us.
Average call time 1423 us.
Average call time 1975 us.
Average call time 2530 us.
Average call time 3045 us.
Average call time 3599 us.
Average call time 4034 us.
Average call time 4523 us.
Average call time 5129 us.

Есть ли способ диагностировать эту проблему?

Есть ли какое-либо решение или альтернатива, которая эффективно работает на Java 10?

ПРИМЕЧАНИЕ. Мы можем написать до конца файла, однако для этого потребуется заблокировать его, чего мы хотим избежать.

Для сравнения: в Windows 10, Java 8 (не tmpfs)

Average call time 542 us.
Average call time 487 us.
Average call time 480 us.
Average call time 490 us.
Average call time 507 us.
Average call time 559 us.
Average call time 498 us.
Average call time 526 us.
Average call time 489 us.
Average call time 504 us.

Windows 10, Java 10.0.1

Average call time 586 us.
Average call time 508 us.
Average call time 615 us.
Average call time 599 us.
Average call time 580 us.
Average call time 577 us.
Average call time 557 us.
Average call time 572 us.
Average call time 578 us.
Average call time 554 us.

UPDATE Похоже, что выбор системного вызова изменился между Java 8 и 10. Это можно увидеть, добавив strace -f в начало командной строки

В Java 8 во внутреннем цикле повторяются следующие вызовы

[pid 49027] ftruncate(23, 53248)        = 0
[pid 49027] lseek(23, 0, SEEK_SET)      = 0
[pid 49027] lseek(23, 0, SEEK_CUR)      = 0

В Java 10 повторяются следующие вызовы

[pid   444] fstat(8, {st_mode=S_IFREG|0664, st_size=126976, ...}) = 0
[pid   444] fallocate(8, 0, 0, 131072)  = 0
[pid   444] lseek(8, 0, SEEK_SET)       = 0
[pid   444] lseek(8, 0, SEEK_CUR)       = 0

В частности, fallocate выполняет гораздо большую работу, чем ftruncate и время, которое, по-видимому, пропорционально длине файла, а не длине, добавленной в файл.

Одна работа вокруг;

  • использовать отражение в дескрипторе файла fd
  • используйте JNA или FFI для вызова ftruncate.

Это похоже на хакерское решение. Есть ли лучшая альтернатива в Java 10?

Ответ 1

Есть ли способ диагностировать эту проблему?

Вы можете использовать профилировщик Java, ориентированный на ядро, например async-profiler.

Вот что он показывает для JDK 8:

JDK 8 profile for RandomAccessFile.setLength

и для JDK 10:

JDK 10 profile for RandomAccessFile.setLength

Профили подтверждают ваш вывод о том, что RandomAccessFile.setLength использует ftruncate ftruncate на JDK 8, но гораздо более тяжелый fallocate на JDK 10.

ftruncate действительно быстр, поскольку он только обновляет метаданные файлов, в то время как fallocate действительно выделяет дисковое пространство (или физическую память в случае tmpfs).

Это изменение было сделано в попытке исправить JDK-8168628: SIGBUS при расширении размера файла для его отображения. Но позже было осознано, что это плохая идея, и исправление было отменено в JDK 11: JDK-8202261.

Есть ли какое-либо решение или альтернатива, которая эффективно работает на Java 10?

Существует внутренний класс sun.nio.ch.FileDispatcherImpl который имеет статический метод truncate0. Он использует ftruncate ftruncate под капотом. Вы можете назвать это через Reflection, имея в виду, что это частный неподдерживаемый API.

Class<?> c = Class.forName("sun.nio.ch.FileDispatcherImpl");
Method m = c.getDeclaredMethod("truncate0", FileDescriptor.class, long.class);
m.setAccessible(true);
m.invoke(null, raf.getFD(), length);