Java obtener el tamaño del archivo de manera eficiente

166

Mientras busco en Google, veo que el uso java.io.File#length()puede ser lento. FileChanneltiene un size()método que también está disponible.

¿Hay alguna manera eficiente en Java para obtener el tamaño del archivo?

joshjdevl
fuente
77
¿Puedes proporcionar los enlaces que dicen que File.length () "puede ser lento"?
mate b
1
lo siento, aquí está el enlace javaperformancetuning.com/tips/rawtips.shtml buscar "La información del archivo como File.length () requiere una llamada al sistema y puede ser lenta". Es realmente una declaración confusa, parece casi asumido que sería una llamada al sistema.
joshjdevl
25
Obtener la longitud del archivo requerirá una llamada al sistema sin importar cómo lo haga. Puede ser lento si está sobre una red o algún otro sistema de archivos muy lento. No hay una forma más rápida de obtenerlo que File.length (), y la definición de "lento" aquí solo significa que no lo llame innecesariamente.
jsight
Creo que eso es lo que GHad estaba tratando de probar a continuación. Mis resultados son (en ubuntu 8.04): solo una URL de acceso es más rápida. 5 carreras, 50 iteraciones ¿CANAL es el más rápido confuso hasta ahora? :) para mis propósitos, solo haré un acceso. aunque es extraño? que obtuvimos resultados diferentes
joshjdevl
1
Esta operación puede ser muy lenta si la información está en el disco en lugar de en la memoria caché. (como 1000 veces más lento), sin embargo, hay poco que pueda hacer al respecto que no sea asegurarse de que la información que necesita esté siempre en caché (como precargarla y tener suficiente memoria para que permanezca en la memoria)
Peter Lawrey

Respuestas:

102

Bueno, intenté medirlo con el siguiente código:

Para carreras = 1 e iteraciones = 1, el método de URL es más rápido la mayoría de las veces seguido de canal. Ejecuto esto con una pausa fresca unas 10 veces. Entonces, para acceder una vez, usar la URL es la forma más rápida en la que puedo pensar:

LENGTH sum: 10626, per Iteration: 10626.0

CHANNEL sum: 5535, per Iteration: 5535.0

URL sum: 660, per Iteration: 660.0

Para carreras = 5 e iteraciones = 50, la imagen se dibuja de manera diferente.

LENGTH sum: 39496, per Iteration: 157.984

CHANNEL sum: 74261, per Iteration: 297.044

URL sum: 95534, per Iteration: 382.136

El archivo debe almacenar en caché las llamadas al sistema de archivos, mientras que los canales y la URL tienen algo de sobrecarga.

Código:

import java.io.*;
import java.net.*;
import java.util.*;

public enum FileSizeBench {

    LENGTH {
        @Override
        public long getResult() throws Exception {
            File me = new File(FileSizeBench.class.getResource(
                    "FileSizeBench.class").getFile());
            return me.length();
        }
    },
    CHANNEL {
        @Override
        public long getResult() throws Exception {
            FileInputStream fis = null;
            try {
                File me = new File(FileSizeBench.class.getResource(
                        "FileSizeBench.class").getFile());
                fis = new FileInputStream(me);
                return fis.getChannel().size();
            } finally {
                fis.close();
            }
        }
    },
    URL {
        @Override
        public long getResult() throws Exception {
            InputStream stream = null;
            try {
                URL url = FileSizeBench.class
                        .getResource("FileSizeBench.class");
                stream = url.openStream();
                return stream.available();
            } finally {
                stream.close();
            }
        }
    };

    public abstract long getResult() throws Exception;

    public static void main(String[] args) throws Exception {
        int runs = 5;
        int iterations = 50;

        EnumMap<FileSizeBench, Long> durations = new EnumMap<FileSizeBench, Long>(FileSizeBench.class);

        for (int i = 0; i < runs; i++) {
            for (FileSizeBench test : values()) {
                if (!durations.containsKey(test)) {
                    durations.put(test, 0l);
                }
                long duration = testNow(test, iterations);
                durations.put(test, durations.get(test) + duration);
                // System.out.println(test + " took: " + duration + ", per iteration: " + ((double)duration / (double)iterations));
            }
        }

        for (Map.Entry<FileSizeBench, Long> entry : durations.entrySet()) {
            System.out.println();
            System.out.println(entry.getKey() + " sum: " + entry.getValue() + ", per Iteration: " + ((double)entry.getValue() / (double)(runs * iterations)));
        }

    }

    private static long testNow(FileSizeBench test, int iterations)
            throws Exception {
        long result = -1;
        long before = System.nanoTime();
        for (int i = 0; i < iterations; i++) {
            if (result == -1) {
                result = test.getResult();
                //System.out.println(result);
            } else if ((result = test.getResult()) != result) {
                 throw new Exception("variance detected!");
             }
        }
        return (System.nanoTime() - before) / 1000;
    }

}
GHad
fuente
1
Parece que la forma de URL es la mejor opción para el acceso único, ya sea XP o Linux. Greetz GHad
GHad
73
stream.available()no devuelve la longitud del archivo. Devuelve la cantidad de bytes que están disponibles para leer sin bloquear otras secuencias. No es necesariamente la misma cantidad de bytes que la longitud del archivo. Para obtener la longitud real de una secuencia, realmente necesita leerla (y contar los bytes leídos mientras tanto).
BalusC
11
Este punto de referencia es o más bien su interpretación no es correcta. En el recuento de iteraciones bajas, las pruebas posteriores aprovechan el almacenamiento en caché de archivos del sistema operativo. En la prueba de iteraciones superiores, la clasificación es correcta, pero no porque File.length () esté almacenando en caché algo, sino simplemente porque las otras 2 opciones se basan en el mismo método pero hacen un trabajo adicional que las ralentiza.
x4u
2
@Paolo, el almacenamiento en caché y la optimización del acceso al sistema de archivos es una de las principales responsabilidades de un sistema operativo. faqs.org/docs/linux_admin/buffer-cache.html Para obtener buenos resultados de evaluación comparativa, el caché debe borrarse antes de cada ejecución.
z0r
3
Más allá de lo que dice el javadoc para InputStream.available (), el hecho de que el método disponible () devuelva un int debería ser una bandera roja contra el enfoque de URL. Pruébelo con un archivo de 3GB y será obvio que no es una forma válida de determinar la longitud del archivo.
Scrubbie
32

El punto de referencia proporcionado por GHad mide muchas otras cosas (como la reflexión, la creación de instancias de objetos, etc.) además de obtener la longitud. Si intentamos deshacernos de estas cosas, en una llamada obtengo los siguientes tiempos en microsegundos:

   suma de archivos ___ 19.0, por iteración ___ 19.0
    suma de raf ___ 16.0, por iteración ___ 16.0
suma de canales__273.0, por iteración__273.0

Por 100 ejecuciones y 10000 iteraciones obtengo:

   suma de archivos__1767629.0, por iteración__1.7676290000000001
    suma raf ___ 881284.0, por iteración__0.8812840000000001
suma de canales ___ 414286.0, por iteración__0.414286

Ejecuté el siguiente código modificado dando como argumento el nombre de un archivo de 100MB.

import java.io.*;
import java.nio.channels.*;
import java.net.*;
import java.util.*;

public class FileSizeBench {

  private static File file;
  private static FileChannel channel;
  private static RandomAccessFile raf;

  public static void main(String[] args) throws Exception {
    int runs = 1;
    int iterations = 1;

    file = new File(args[0]);
    channel = new FileInputStream(args[0]).getChannel();
    raf = new RandomAccessFile(args[0], "r");

    HashMap<String, Double> times = new HashMap<String, Double>();
    times.put("file", 0.0);
    times.put("channel", 0.0);
    times.put("raf", 0.0);

    long start;
    for (int i = 0; i < runs; ++i) {
      long l = file.length();

      start = System.nanoTime();
      for (int j = 0; j < iterations; ++j)
        if (l != file.length()) throw new Exception();
      times.put("file", times.get("file") + System.nanoTime() - start);

      start = System.nanoTime();
      for (int j = 0; j < iterations; ++j)
        if (l != channel.size()) throw new Exception();
      times.put("channel", times.get("channel") + System.nanoTime() - start);

      start = System.nanoTime();
      for (int j = 0; j < iterations; ++j)
        if (l != raf.length()) throw new Exception();
      times.put("raf", times.get("raf") + System.nanoTime() - start);
    }
    for (Map.Entry<String, Double> entry : times.entrySet()) {
        System.out.println(
            entry.getKey() + " sum: " + 1e-3 * entry.getValue() +
            ", per Iteration: " + (1e-3 * entry.getValue() / runs / iterations));
    }
  }
}
basilikode
fuente
3
en realidad, si bien tiene razón al decir que mide otros aspectos, debería ser más claro en mi pregunta. Estoy buscando obtener el tamaño de archivo de varios archivos, y quiero la forma más rápida posible. así que realmente necesito tener en cuenta la creación de objetos y los gastos generales, ya que ese es un escenario real
joshjdevl
3
Alrededor del 90% del tiempo se gasta en esa cosa getResource. Dudo que necesite usar la reflexión para obtener el nombre de un archivo que contiene algún bytecode de Java.
20

Todos los casos de prueba en esta publicación son defectuosos ya que acceden al mismo archivo para cada método probado. Entonces, las patadas de almacenamiento en caché de disco en las que se benefician las pruebas 2 y 3. Para probar mi punto, tomé el caso de prueba proporcionado por GHAD y cambié el orden de enumeración y a continuación se muestran los resultados.

Mirando el resultado, creo que File.length () es realmente el ganador.

El orden de prueba es el orden de salida. Incluso puede ver que el tiempo empleado en mi máquina varió entre ejecuciones, pero File.Length () cuando no fue el primero, e incurrió en el primer acceso al disco ganado.

---
LENGTH sum: 1163351, per Iteration: 4653.404
CHANNEL sum: 1094598, per Iteration: 4378.392
URL sum: 739691, per Iteration: 2958.764

---
CHANNEL sum: 845804, per Iteration: 3383.216
URL sum: 531334, per Iteration: 2125.336
LENGTH sum: 318413, per Iteration: 1273.652

--- 
URL sum: 137368, per Iteration: 549.472
LENGTH sum: 18677, per Iteration: 74.708
CHANNEL sum: 142125, per Iteration: 568.5
StuartH
fuente
9

Cuando modifico su código para usar un archivo al que se accede por una ruta absoluta en lugar de un recurso, obtengo un resultado diferente (para 1 ejecución, 1 iteración y un archivo de 100,000 bytes; los tiempos para un archivo de 10 bytes son idénticos a 100,000 bytes )

LONGITUD suma: 33, por iteración: 33.0

CANAL suma: 3626, por iteración: 3626.0

Suma de URL: 294, por iteración: 294.0

tgdavies
fuente
9

En respuesta al punto de referencia de rgrig, el tiempo necesario para abrir / cerrar las instancias FileChannel y RandomAccessFile también debe tenerse en cuenta, ya que estas clases abrirán una secuencia para leer el archivo.

Después de modificar el punto de referencia, obtuve estos resultados para 1 iteraciones en un archivo de 85 MB:

file totalTime: 48000 (48 us)
raf totalTime: 261000 (261 us)
channel totalTime: 7020000 (7 ms)

Para 10000 iteraciones en el mismo archivo:

file totalTime: 80074000 (80 ms)
raf totalTime: 295417000 (295 ms)
channel totalTime: 368239000 (368 ms)

Si todo lo que necesita es el tamaño del archivo, file.length () es la forma más rápida de hacerlo. Si planea utilizar el archivo para otros fines, como leer / escribir, RAF parece ser una mejor opción. Simplemente no olvide cerrar la conexión de archivo :-)

import java.io.File;
import java.io.FileInputStream;
import java.io.RandomAccessFile;
import java.nio.channels.FileChannel;
import java.util.HashMap;
import java.util.Map;

public class FileSizeBench
{    
    public static void main(String[] args) throws Exception
    {
        int iterations = 1;
        String fileEntry = args[0];

        Map<String, Long> times = new HashMap<String, Long>();
        times.put("file", 0L);
        times.put("channel", 0L);
        times.put("raf", 0L);

        long fileSize;
        long start;
        long end;
        File f1;
        FileChannel channel;
        RandomAccessFile raf;

        for (int i = 0; i < iterations; i++)
        {
            // file.length()
            start = System.nanoTime();
            f1 = new File(fileEntry);
            fileSize = f1.length();
            end = System.nanoTime();
            times.put("file", times.get("file") + end - start);

            // channel.size()
            start = System.nanoTime();
            channel = new FileInputStream(fileEntry).getChannel();
            fileSize = channel.size();
            channel.close();
            end = System.nanoTime();
            times.put("channel", times.get("channel") + end - start);

            // raf.length()
            start = System.nanoTime();
            raf = new RandomAccessFile(fileEntry, "r");
            fileSize = raf.length();
            raf.close();
            end = System.nanoTime();
            times.put("raf", times.get("raf") + end - start);
        }

        for (Map.Entry<String, Long> entry : times.entrySet()) {
            System.out.println(entry.getKey() + " totalTime: " + entry.getValue() + " (" + getTime(entry.getValue()) + ")");
        }
    }

    public static String getTime(Long timeTaken)
    {
        if (timeTaken < 1000) {
            return timeTaken + " ns";
        } else if (timeTaken < (1000*1000)) {
            return timeTaken/1000 + " us"; 
        } else {
            return timeTaken/(1000*1000) + " ms";
        } 
    }
}
Karthikeyan
fuente
8

Me encontré con este mismo problema. Necesitaba obtener el tamaño del archivo y la fecha de modificación de 90,000 archivos en un recurso compartido de red. Usando Java, y siendo lo más minimalista posible, tomaría mucho tiempo. (Necesitaba obtener la URL del archivo y la ruta del objeto también. Por lo tanto, varió un poco, pero más de una hora). Luego utilicé un ejecutable Win32 nativo, e hice la misma tarea, simplemente volqué el archivo ruta, modificado y tamaño a la consola, y ejecutó eso desde Java. La velocidad fue asombrosa. El proceso nativo y el manejo de mi cadena para leer los datos podrían procesar más de 1000 elementos por segundo.

Entonces, aunque las personas clasificaron el comentario anterior, esta es una solución válida y resolvió mi problema. En mi caso, sabía de antemano las carpetas que necesitaba los tamaños, y podría pasar eso en la línea de comandos a mi aplicación win32. Pasé de horas para procesar un directorio a minutos.

El problema también parecía ser específico de Windows. OS X no tenía el mismo problema y podía acceder a la información de los archivos de red tan rápido como el SO podía hacerlo.

El manejo de archivos Java en Windows es terrible. Sin embargo, el acceso al disco local para los archivos está bien. Fueron solo los recursos compartidos de red los que causaron el terrible rendimiento. Windows también podría obtener información sobre el recurso compartido de red y calcular el tamaño total en menos de un minuto.

--Ben

Ben Spink
fuente
3

Si desea el tamaño de archivo de varios archivos en un directorio, use Files.walkFileTree. Puede obtener el tamaño del BasicFileAttributesque recibirá.

Esto es mucho más rápido que invocar .length()el resultado de File.listFiles()o usar Files.size()el resultado de Files.newDirectoryStream(). En mis casos de prueba fue aproximadamente 100 veces más rápido.

Scg
fuente
FYI, Files.walkFileTreeestá disponible en Android 26+.
Joshua Pinter
2

En realidad, creo que el "ls" puede ser más rápido. Definitivamente, hay algunos problemas en Java relacionados con la obtención de información de archivo. Lamentablemente, no existe un método seguro equivalente de ls recursivo para Windows. (DIR / S de cmd.exe puede confundirse y generar errores en bucles infinitos)

En XP, al acceder a un servidor en la LAN, me toma 5 segundos en Windows obtener el recuento de los archivos en una carpeta (33,000) y el tamaño total.

Cuando itero recursivamente a través de esto en Java, me lleva más de 5 minutos. Comencé a medir el tiempo que toma hacer file.length (), file.lastModified () y file.toURI () y lo que descubrí es que esas 3 llamadas me toman el 99% de mi tiempo. Las 3 llamadas que realmente necesito hacer ...

La diferencia para 1000 archivos es de 15 ms local frente a 1800 ms en el servidor. El escaneo de la ruta del servidor en Java es ridículamente lento. Si el sistema operativo nativo puede ser rápido al escanear esa misma carpeta, ¿por qué no puede Java?

Como una prueba más completa, utilicé WineMerge en XP para comparar la fecha de modificación y el tamaño de los archivos en el servidor versus los archivos localmente. Esto iteraba sobre el árbol de directorios completo de 33,000 archivos en cada carpeta. Tiempo total, 7 segundos. Java: más de 5 minutos.

Por lo tanto, la declaración original y la pregunta del OP son verdaderas y válidas. Es menos notable cuando se trata de un sistema de archivos local. Hacer una comparación local de la carpeta con 33,000 elementos lleva 3 segundos en WinMerge y 32 segundos localmente en Java. De nuevo, java versus native es una desaceleración 10x en estas pruebas rudimentarias.

Java 1.6.0_22 (más reciente), LAN Gigabit y conexiones de red, el ping es inferior a 1 ms (ambos en el mismo conmutador)

Java es lento

Ben Spink
fuente
2
Esto también parece ser específico del sistema operativo. Al hacer la misma aplicación java yendo después de la misma carpeta de OS X usando samba, tardó 26 segundos en enumerar los 33,000 elementos, tamaños y fechas completos. Entonces, ¿la red Java es lenta en Windows entonces? (OS X era java 1.6.0_22 también.)
Ben Spink
2

Desde el punto de referencia de GHad, hay algunos problemas que la gente ha mencionado:

1> Como BalusC mencionó: stream.available () fluye en este caso.

Porque disponible () devuelve una estimación del número de bytes que se pueden leer (u omitir) de esta secuencia de entrada sin bloquear mediante la próxima invocación de un método para esta secuencia de entrada.

Así que primero para eliminar la URL de este enfoque.

2> Como mencionó StuartH: el orden en que se ejecuta la prueba también hace la diferencia de caché, así que sáquelo ejecutando la prueba por separado.


Ahora comience la prueba:

Cuando el CANAL uno corre solo:

CHANNEL sum: 59691, per Iteration: 238.764

Cuando LONGITUD se corre solo:

LENGTH sum: 48268, per Iteration: 193.072

Parece que LENGTH one es el ganador aquí:

@Override
public long getResult() throws Exception {
    File me = new File(FileSizeBench.class.getResource(
            "FileSizeBench.class").getFile());
    return me.length();
}
Gob00st
fuente