RandomAccessFile.setLength намного медленнее в Java 10 (Centos) - PullRequest
0 голосов
/ 21 мая 2018

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

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.

ОБНОВЛЕНИЕ Похоже, что выбор системного вызова изменился между 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 Ответ

0 голосов
/ 22 мая 2018

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

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

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

JDK 8 profile for RandomAccessFile.setLength

и для JDK 10:

JDK 10 profile for RandomAccessFile.setLength

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

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

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

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

Существует внутренний класс sun.nio.ch.FileDispatcherImpl, который имеет статический метод truncate0.Он использует 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);
...