RandomAccessFile.setLength mucho más lento en Java 10(Centos)
java-10 (1)
El siguiente codigo
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.");
}
}
}
En Java 8, esto funciona bien (el archivo está en tmpfs, por lo que es de esperar que sea trivial)
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.
En Java 10, esto se vuelve cada vez más lento a medida que el archivo crece.
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.
¿Hay alguna manera de diagnosticar este tipo de problema?
¿Hay alguna solución o alternativa que funcione de manera eficiente en Java 10?
NOTA: Podríamos escribir al final del archivo, sin embargo, esto requeriría bloquearlo, lo que queremos evitar.
Para comparación, en Windows 10, Java 8 (no 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.
ACTUALIZACIÓN Parece que la elección de la llamada al sistema ha cambiado entre Java 8 y 10. Esto se puede ver al strace -f
al inicio de la línea de comando
En Java 8, las siguientes llamadas se repiten en el bucle interno
[pid 49027] ftruncate(23, 53248) = 0
[pid 49027] lseek(23, 0, SEEK_SET) = 0
[pid 49027] lseek(23, 0, SEEK_CUR) = 0
En Java 10, se repiten las siguientes llamadas.
[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
En particular, fallocate
hace mucho más trabajo que ftruncate
y el tiempo empleado parece ser proporcional a la longitud del archivo, no a la longitud agregada al archivo.
Un trabajo alrededor es
- utilizar la reflexión para el descriptor de archivos
fd
- Usa JNA o FFI para llamar a ftruncate.
Esto parece una solución hacky. ¿Hay alguna alternativa mejor en Java 10?
¿Hay alguna manera de diagnosticar este tipo de problema?
Puede utilizar el perfilador de Java compatible con el kernel como async-profiler .
Esto es lo que muestra para JDK 8:
y para JDK 10:
Los perfiles confirman su conclusión de que RandomAccessFile.setLength
usa ftruncate
syscall en JDK 8, pero una mucho mayor fallocate
en JDK 10.
ftruncate
es realmente rápido, ya que solo actualiza los metadatos del archivo, mientras que la asignación de fallocate
asigna espacio de disco (o memoria física en caso de tmpfs
).
Este cambio se realizó en un intento de reparar JDK-8168628 : SIGBUS al extender el tamaño del archivo para JDK-8168628 . Pero luego se dio cuenta de que esta es una mala idea, y la solución se revirtió en JDK 11: JDK-8202261 .
¿Hay alguna solución o alternativa que funcione de manera eficiente en Java 10?
Hay una clase interna sun.nio.ch.FileDispatcherImpl
que tiene el método truncate0
estático. Utiliza ftruncate
ftruncate bajo el capó. Puede llamarlo a través de Reflection, teniendo en cuenta que se trata de una API privada no compatible.
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);