Java erhält Dateigröße effizient

166

Beim googeln sehe ich, dass die Verwendung java.io.File#length()langsam sein kann. FileChannelhat eine size()Methode, die ebenfalls verfügbar ist.

Gibt es in Java eine effiziente Möglichkeit, die Dateigröße zu ermitteln?

joshjdevl
quelle
7
Können Sie die Links angeben, die besagen, dass File.length () "langsam sein kann"?
Matt B
1
Entschuldigung, hier ist der Link javaperformancetuning.com/tips/rawtips.shtml Suche nach "Dateiinformationen wie File.length () erfordern einen Systemaufruf und können langsam sein." Es ist wirklich eine verwirrende Aussage, es scheint fast angenommen, dass es sich um einen Systemaufruf handelt.
Joshjdevl
25
Um die Dateilänge zu ermitteln, ist ein Systemaufruf erforderlich, unabhängig davon, wie Sie dies tun. Es kann langsam sein, wenn es über ein Netzwerk oder ein anderes sehr langsames Dateisystem läuft. Es gibt keinen schnelleren Weg, um es zu bekommen als File.length (), und die Definition von "langsam" bedeutet hier nur, dass Sie es nicht unnötig aufrufen müssen.
Jsight
Ich denke, das hat GHad unten versucht zu testen. Meine Ergebnisse sind (unter Ubuntu 8.04): Nur eine Zugriffs-URL ist am schnellsten. 5 Läufe, 50 Iterationen CHANNEL ist noch am schnellsten verwirrend? :) Für meine Zwecke mache ich nur einen Zugriff. obwohl es seltsam ist? dass wir unterschiedliche Ergebnisse haben
joshjdevl
1
Dieser Vorgang kann sehr langsam sein, wenn sich die Informationen auf der Festplatte und nicht im Cache befinden. (wie 1000x langsamer) Sie können jedoch nur wenig dagegen tun, außer sicherzustellen, dass sich die benötigten Informationen immer im Cache befinden (z. B. vor dem Laden und genügend Speicher, damit sie im Speicher
bleiben

Antworten:

102

Nun, ich habe versucht, es mit dem folgenden Code zu messen:

Bei Läufen = 1 und Iterationen = 1 ist die URL-Methode meistens am schnellsten, gefolgt vom Kanal. Ich führe dies mit einer Pause etwa 10 Mal frisch. Für einen einmaligen Zugriff ist die Verwendung der URL der schnellste Weg, den ich mir vorstellen kann:

LENGTH sum: 10626, per Iteration: 10626.0

CHANNEL sum: 5535, per Iteration: 5535.0

URL sum: 660, per Iteration: 660.0

Für Läufe = 5 und Iterationen = 50 zeichnet das Bild anders.

LENGTH sum: 39496, per Iteration: 157.984

CHANNEL sum: 74261, per Iteration: 297.044

URL sum: 95534, per Iteration: 382.136

Die Datei muss die Aufrufe an das Dateisystem zwischenspeichern, während Kanäle und URL einen gewissen Overhead haben.

Code:

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
quelle
1
Scheint, als wäre der URL-Weg der beste für den Einzelzugriff, egal ob XP oder Linux. Greetz GHad
GHad
73
stream.available()gibt die Dateilänge nicht zurück. Es gibt die Anzahl der Bytes zurück, die zum Lesen verfügbar sind, ohne andere Streams zu blockieren. Es ist nicht unbedingt die gleiche Anzahl von Bytes wie die Dateilänge. Um die tatsächliche Länge eines Streams zu erhalten, müssen Sie ihn wirklich lesen (und in der Zwischenzeit die gelesenen Bytes zählen).
BalusC
11
Dieser Benchmark ist oder vielmehr ist seine Interpretation nicht korrekt. Bei der geringen Iterationszahl nutzen die späteren Tests das Datei-Caching des Betriebssystems. Im Test mit höheren Iterationen ist das Ranking korrekt, aber nicht, weil File.length () etwas zwischenspeichert, sondern einfach, weil die anderen beiden Optionen auf derselben Methode basieren, aber zusätzliche Arbeit leisten, die sie verlangsamt.
x4u
2
@Paolo, das Zwischenspeichern und Optimieren des Dateisystemzugriffs ist eine der Hauptaufgaben eines Betriebssystems. faqs.org/docs/linux_admin/buffer-cache.html Um gute Benchmarking-Ergebnisse zu erzielen , sollte der Cache vor jedem Lauf geleert werden.
z0r
3
Abgesehen davon, was das Javadoc für InputStream.available () sagt, sollte die Tatsache, dass die Methode available () ein int zurückgibt, eine rote Fahne gegen den URL-Ansatz sein. Versuchen Sie es mit einer 3-GB-Datei, und es ist offensichtlich, dass dies keine gültige Methode zum Bestimmen der Dateilänge ist.
Scrubbie
32

Der von GHad vorgegebene Benchmark misst neben der Länge viele andere Dinge (wie Reflexion, Instanziierung von Objekten usw.). Wenn wir versuchen, diese Dinge loszuwerden, erhalte ich für einen Anruf die folgenden Zeiten in Mikrosekunden:

   Dateisumme ___ 19,0, pro Iteration ___ 19,0
    raf sum ___ 16.0, per Iteration ___ 16.0
Kanalsumme__273.0, pro Iteration__273.0

Für 100 Läufe und 10000 Iterationen bekomme ich:

   Dateisumme__1767629.0, pro Iteration__1.7676290000000001
    raf sum ___ 881284.0, per Iteration__0.8812840000000001
Kanalsumme ___ 414286.0, pro Iteration__0.414286

Ich habe den folgenden modifizierten Code ausgeführt und als Argument den Namen einer 100-MB-Datei angegeben.

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
quelle
3
Obwohl Sie zu Recht sagen, dass es andere Aspekte misst, sollte ich in meiner Frage klarer sein. Ich möchte die Dateigröße mehrerer Dateien ermitteln und möchte den schnellstmöglichen Weg. Ich muss also wirklich die Objekterstellung und den Overhead berücksichtigen, da dies ein echtes Szenario ist
joshjdevl
3
Ungefähr 90% der Zeit wird in dieser getResource-Sache verbracht. Ich bezweifle, dass Sie Reflection verwenden müssen, um den Namen einer Datei zu erhalten, die Java-Bytecode enthält.
20

Alle Testfälle in diesem Beitrag sind fehlerhaft, da sie für jede getestete Methode auf dieselbe Datei zugreifen. Das Festplatten-Caching beginnt also, von dem die Tests 2 und 3 profitieren. Um meinen Standpunkt zu beweisen, habe ich einen von GHAD bereitgestellten Testfall genommen und die Reihenfolge der Aufzählung geändert. Nachfolgend sind die Ergebnisse aufgeführt.

Mit Blick auf das Ergebnis denke ich, dass File.length () wirklich der Gewinner ist.

Die Reihenfolge des Tests ist die Reihenfolge der Ausgabe. Sie können sogar sehen, dass die auf meinem Computer benötigte Zeit zwischen den Ausführungen variiert, aber File.Length (), wenn nicht zuerst, und der erste gewonnene Festplattenzugriff.

---
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
quelle
9

Wenn ich Ihren Code so ändere, dass anstelle einer Ressource eine Datei verwendet wird, auf die über einen absoluten Pfad zugegriffen wird, erhalte ich ein anderes Ergebnis (für 1 Lauf, 1 Iteration und eine 100.000-Byte-Datei - die Zeiten für eine 10-Byte-Datei sind identisch mit 100.000 Byte )

LÄNGE Summe: 33, pro Iteration: 33,0

CHANNEL-Summe: 3626, pro Iteration: 3626,0

URL-Summe: 294, pro Iteration: 294,0

tgdavies
quelle
9

In Reaktion auf den Benchmark von rgrig muss auch die Zeit berücksichtigt werden, die zum Öffnen / Schließen der FileChannel- und RandomAccessFile-Instanzen benötigt wird, da diese Klassen einen Stream zum Lesen der Datei öffnen.

Nachdem ich den Benchmark geändert hatte, erhielt ich diese Ergebnisse für 1 Iteration in einer 85-MB-Datei:

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

Für 10000 Iterationen in derselben Datei:

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

Wenn Sie nur die Dateigröße benötigen, ist file.length () der schnellste Weg, dies zu tun. Wenn Sie die Datei für andere Zwecke wie Lesen / Schreiben verwenden möchten, ist RAF anscheinend die bessere Wahl. Vergiss nur nicht, die Dateiverbindung zu schließen :-)

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
quelle
8

Ich bin auf dasselbe Problem gestoßen. Ich musste die Dateigröße und das Änderungsdatum von 90.000 Dateien auf einer Netzwerkfreigabe ermitteln. Wenn Sie Java verwenden und so minimalistisch wie möglich sind, würde dies sehr lange dauern. (Ich musste die URL aus der Datei und auch den Pfad des Objekts abrufen. Sie variierte also etwas, aber mehr als eine Stunde.) Dann verwendete ich eine native ausführbare Win32-Datei und erledigte dieselbe Aufgabe, indem ich nur die Datei ablegte Pfad, geändert und Größe zur Konsole und ausgeführt von Java. Die Geschwindigkeit war unglaublich. Der native Prozess und meine Zeichenfolgenbehandlung zum Lesen der Daten können über 1000 Elemente pro Sekunde verarbeiten.

Obwohl die Leute den obigen Kommentar herabgestuft haben, ist dies eine gültige Lösung und hat mein Problem gelöst. In meinem Fall kannte ich die Ordner, deren Größe ich benötigte, im Voraus und konnte diese in der Befehlszeile an meine win32-App übergeben. Ich ging von Stunden, um ein Verzeichnis zu Minuten zu verarbeiten.

Das Problem schien auch Windows-spezifisch zu sein. OS X hatte nicht das gleiche Problem und konnte so schnell wie das Betriebssystem auf Netzwerkdatei-Informationen zugreifen.

Die Handhabung von Java-Dateien unter Windows ist schrecklich. Der lokale Festplattenzugriff für Dateien ist jedoch in Ordnung. Es waren nur Netzwerkfreigaben, die die schreckliche Leistung verursachten. Windows könnte Informationen über die Netzwerkfreigabe erhalten und die Gesamtgröße in weniger als einer Minute berechnen.

- Ben

Ben Spink
quelle
3

Wenn Sie die Dateigröße mehrerer Dateien in einem Verzeichnis möchten, verwenden Sie Files.walkFileTree. Sie können die Größe von der erhalten BasicFileAttributes, die Sie erhalten.

Dies ist viel schneller, als .length()das Ergebnis von aufzurufen File.listFiles()oder Files.size()das Ergebnis von zu verwenden Files.newDirectoryStream(). In meinen Testfällen war es ungefähr 100 mal schneller.

Scg
quelle
Zu Ihrer Information, Files.walkFileTreeist auf Android 26+ verfügbar.
Joshua Pinter
2

Eigentlich denke ich, dass das "ls" schneller sein kann. In Java gibt es definitiv einige Probleme beim Abrufen von Dateiinformationen. Leider gibt es für Windows keine gleichwertige sichere Methode für rekursives ls. (cmd.exes DIR / S kann verwirrt werden und Fehler in Endlosschleifen erzeugen.)

Unter XP, wenn ich auf einen Server im LAN zugreife, brauche ich unter Windows 5 Sekunden, um die Anzahl der Dateien in einem Ordner (33.000) und die Gesamtgröße zu ermitteln.

Wenn ich dies in Java rekursiv durchlaufe, dauert es über 5 Minuten. Ich habe angefangen, die Zeit zu messen, die für file.length (), file.lastModified () und file.toURI () benötigt wird. Dabei habe ich festgestellt, dass 99% meiner Zeit für diese drei Aufrufe benötigt werden. Die 3 Anrufe, die ich eigentlich machen muss ...

Der Unterschied für 1000 Dateien beträgt 15 ms lokal gegenüber 1800 ms auf dem Server. Das Scannen von Serverpfaden in Java ist lächerlich langsam. Wenn das native Betriebssystem denselben Ordner schnell scannen kann, warum kann Java dann nicht?

Als vollständigeren Test habe ich WineMerge unter XP verwendet, um das Änderungsdatum und die Größe der Dateien auf dem Server mit den Dateien vor Ort zu vergleichen. Dies wurde über den gesamten Verzeichnisbaum von 33.000 Dateien in jedem Ordner wiederholt. Gesamtzeit 7 Sekunden. Java: über 5 Minuten.

Die ursprüngliche Aussage und Frage des OP ist also wahr und gültig. Es ist weniger auffällig, wenn es sich um ein lokales Dateisystem handelt. Das lokale Vergleichen des Ordners mit 33.000 Elementen dauert in WinMerge 3 Sekunden und in Java 32 Sekunden lokal. Java versus Native ist also eine 10-fache Verlangsamung in diesen rudimentären Tests.

Java 1.6.0_22 (aktuell), Gigabit LAN und Netzwerkverbindungen, Ping ist weniger als 1 ms (beide im selben Switch)

Java ist langsam.

Ben Spink
quelle
2
Dies scheint auch betriebssystemspezifisch zu sein. Wenn Sie dieselbe Java-App mit Samba nach demselben Ordner unter OS X ausführen, dauerte es 26 Sekunden, bis die gesamten 33.000 Elemente, Größen und Daten aufgelistet waren. Also ist Netzwerk-Java unter Windows nur langsam? (OS X war auch Java 1.6.0_22.)
Ben Spink
2

Aus dem GHad-Benchmark sind einige Punkte hervorgegangen:

1> Wie von BalusC erwähnt: stream.available () wird in diesem Fall übertragen.

Weil available () eine Schätzung der Anzahl von Bytes zurückgibt , die aus diesem Eingabestream gelesen (oder übersprungen) werden können, ohne beim nächsten Aufruf einer Methode für diesen Eingabestream blockiert zu werden.

Also zuerst die URL dieses Ansatzes entfernen.

2> Wie StuartH erwähnt hat - die Reihenfolge, in der der Testlauf ausgeführt wird, macht auch den Cache-Unterschied aus. Nehmen Sie dies heraus, indem Sie den Test separat ausführen.


Starten Sie nun den Test:

Beim CHANNEL läuft man alleine:

CHANNEL sum: 59691, per Iteration: 238.764

Bei LÄNGE läuft man alleine:

LENGTH sum: 48268, per Iteration: 193.072

Sieht also so aus, als wäre die LÄNGE hier der Gewinner:

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