Wie bestimmen Sie die ideale Puffergröße bei Verwendung von FileInputStream?

156

Ich habe eine Methode, die aus einer Datei ein MessageDigest (einen Hash) erstellt, und ich muss dies für viele Dateien tun (> = 100.000). Wie groß sollte der Puffer sein, der zum Lesen aus den Dateien verwendet wird, um die Leistung zu maximieren?

Fast jeder kennt den Basiscode (den ich hier für alle Fälle wiederholen werde):

MessageDigest md = MessageDigest.getInstance( "SHA" );
FileInputStream ios = new FileInputStream( "myfile.bmp" );
byte[] buffer = new byte[4 * 1024]; // what should this value be?
int read = 0;
while( ( read = ios.read( buffer ) ) > 0 )
    md.update( buffer, 0, read );
ios.close();
md.digest();

Was ist die ideale Größe des Puffers, um den Durchsatz zu maximieren? Ich weiß, dass dies systemabhängig ist, und ich bin mir ziemlich sicher, dass das Betriebssystem, das Dateisystem und die Festplatte abhängig sind und dass möglicherweise andere Hardware / Software im Mix enthalten ist.

(Ich sollte darauf hinweisen, dass ich etwas neu in Java bin, daher kann dies nur ein Java-API-Aufruf sein, von dem ich nichts weiß.)

Bearbeiten: Ich weiß nicht im Voraus, auf welchen Systemen dies verwendet wird, daher kann ich nicht viel davon ausgehen. (Ich benutze Java aus diesem Grund.)

Bearbeiten: Dem obigen Code fehlen Dinge wie try..catch, um den Beitrag kleiner zu machen

ARKBAN
quelle

Antworten:

213

Die optimale Puffergröße hängt mit einer Reihe von Faktoren zusammen: Dateisystemblockgröße, CPU-Cache-Größe und Cache-Latenz.

Die meisten Dateisysteme sind für die Verwendung von Blockgrößen von 4096 oder 8192 konfiguriert. Wenn Sie Ihre Puffergröße so konfigurieren, dass Sie einige Bytes mehr als den Plattenblock lesen, können die Vorgänge mit dem Dateisystem theoretisch äußerst ineffizient sein (dh wenn Sie dies tun) Wenn Sie Ihren Puffer so konfiguriert haben, dass er jeweils 4100 Byte liest, würde jeder Lesevorgang 2 Blocklesevorgänge durch das Dateisystem erfordern. Wenn sich die Blöcke bereits im Cache befinden, zahlen Sie den Preis für RAM -> L3 / L2-Cache-Latenz. Wenn Sie Pech haben und die Blöcke noch nicht im Cache sind, zahlen Sie auch den Preis für die Disk-> RAM-Latenz.

Aus diesem Grund sehen Sie die meisten Puffer mit einer Größe von 2 und im Allgemeinen größer (oder gleich) der Plattenblockgröße. Dies bedeutet, dass einer Ihrer Stream-Lesevorgänge zu mehreren Lesevorgängen von Festplattenblöcken führen kann. Diese Lesevorgänge verwenden jedoch immer einen vollständigen Block - keine verschwendeten Lesevorgänge.

Dies ist in einem typischen Streaming-Szenario ziemlich ausgeglichen, da der Block, der von der Festplatte gelesen wird, beim nächsten Lesen immer noch im Speicher verbleibt (wir führen hier schließlich sequentielle Lesevorgänge durch) Bezahlen des RAM -> L3 / L2-Cache-Latenzpreises beim nächsten Lesevorgang, nicht jedoch der Festplatten-> RAM-Latenz. In Bezug auf die Größenordnung ist die Disk-> RAM-Latenz so langsam, dass sie jede andere Latenz, mit der Sie möglicherweise zu tun haben, ziemlich überfüllt.

Ich vermute also, dass Sie, wenn Sie einen Test mit verschiedenen Cache-Größen durchgeführt haben (dies nicht selbst getan haben), wahrscheinlich einen großen Einfluss der Cache-Größe bis zur Größe des Dateisystemblocks feststellen werden. Darüber hinaus vermute ich, dass sich die Dinge ziemlich schnell beruhigen würden.

Hier gibt es eine Menge Bedingungen und Ausnahmen - die Komplexität des Systems ist tatsächlich erstaunlich (nur L3 -> L2-Cache-Übertragungen in den Griff zu bekommen, ist erstaunlich komplex und ändert sich mit jedem CPU-Typ).

Dies führt zu der Antwort aus der realen Welt: Wenn Ihre App zu 99% verfügbar ist, stellen Sie die Cache-Größe auf 8192 ein und fahren Sie fort (noch besser, wählen Sie die Kapselung gegenüber der Leistung und verwenden Sie BufferedInputStream, um die Details auszublenden). Wenn Sie zu 1% der Apps gehören, die stark vom Festplattendurchsatz abhängig sind, erstellen Sie Ihre Implementierung so, dass Sie verschiedene Strategien für die Festplatteninteraktion austauschen und die Regler und Wählscheiben bereitstellen können, mit denen Ihre Benutzer testen und optimieren können (oder einige davon entwickeln können) selbstoptimierendes System).

Kevin Day
quelle
3
Ich habe für meine Android-App auf einem Mobiltelefon (Nexus 5X) ein Banchmarking durchgeführt: sowohl für kleine Dateien (3,5 MB) als auch für große Dateien (175 MB). Und fand heraus, dass die goldene Größe Byte [] von 524288 Längen sein würde. Nun, Sie können 10-20 ms gewinnen, wenn Sie je nach Dateigröße zwischen kleinem Puffer 4 KB und großem Puffer 524 KB wechseln, aber es lohnt sich nicht. Also waren 524 Kb die beste Option in meinem Fall.
Kirill Karmazin
19

Ja, es hängt wahrscheinlich von verschiedenen Dingen ab - aber ich bezweifle, dass es einen großen Unterschied machen wird. Ich tendiere dazu, mich für 16K oder 32K zu entscheiden, um ein gutes Gleichgewicht zwischen Speichernutzung und Leistung zu finden.

Beachten Sie, dass Sie einen try / finally-Block im Code haben sollten, um sicherzustellen, dass der Stream geschlossen ist, auch wenn eine Ausnahme ausgelöst wird.

Jon Skeet
quelle
Ich habe den Beitrag über den try..catch bearbeitet. In meinem echten Code habe ich einen, aber ich habe ihn weggelassen, um den Beitrag kürzer zu machen.
ARKBAN
1
Wenn wir eine feste Größe dafür definieren wollen, welche Größe ist besser? 4k, 16k oder 32k?
BattleTested
2
@MohammadrezaPanahi: Bitte verwenden Sie keine Kommentare, um Benutzer zu belästigen. Sie haben weniger als eine Stunde vor einem zweiten Kommentar gewartet . Bitte denken Sie daran, dass Benutzer leicht schlafen oder in Besprechungen sein können oder im Grunde genommen mit anderen Dingen beschäftigt sind und keine Verpflichtung haben, Kommentare zu beantworten. Aber um Ihre Frage zu beantworten: Es kommt ganz auf den Kontext an. Wenn Sie auf einem System mit sehr wenig Speicher ausgeführt werden, möchten Sie wahrscheinlich einen kleinen Puffer. Wenn Sie auf einem großen System arbeiten, wird durch die Verwendung eines größeren Puffers die Anzahl der Leseaufrufe verringert. Die Antwort von Kevin Day ist sehr gut.
Jon Skeet
7

In den meisten Fällen spielt es wirklich keine Rolle. Wählen Sie einfach eine gute Größe wie 4K oder 16K und bleiben Sie dabei. Wenn Sie positiv , dass dies der Engpass in der Anwendung ist, dann sollten Sie beginnen , Profilieren die optimale Puffergröße zu finden. Wenn Sie eine zu kleine Größe auswählen, verschwenden Sie Zeit mit zusätzlichen E / A-Vorgängen und zusätzlichen Funktionsaufrufen. Wenn Sie eine zu große Größe auswählen, werden Sie viele Cache-Fehler sehen, die Sie wirklich verlangsamen. Verwenden Sie keinen Puffer, der größer als Ihre L2-Cache-Größe ist.

Adam Rosenfield
quelle
4

Im Idealfall sollten wir genügend Speicher haben, um die Datei in einem Lesevorgang zu lesen. Dies wäre die beste Leistung, da das System das Dateisystem, die Zuordnungseinheiten und die Festplatte nach Belieben verwalten kann. In der Praxis haben Sie das Glück, die Dateigrößen im Voraus zu kennen. Verwenden Sie einfach die durchschnittliche Dateigröße, die auf 4 KB aufgerundet ist (Standardzuweisungseinheit unter NTFS). Und das Beste: Erstellen Sie einen Benchmark, um mehrere Optionen zu testen.

Ovidiu Pacurar
quelle
Meinst du, die beste Puffergröße zum Lesen und Schreiben in einer Datei ist 4k?
BattleTested
4

Sie können die BufferedStreams / Reader verwenden und dann deren Puffergrößen verwenden.

Ich glaube, die BufferedXStreams verwenden 8192 als Puffergröße, aber wie Ovidiu sagte, sollten Sie wahrscheinlich einen Test für eine ganze Reihe von Optionen durchführen. Es wird wirklich von der Dateisystem- und Festplattenkonfiguration abhängen, welche die besten Größen sind.

John Gardner
quelle
4

Das Lesen von Dateien mit FileChannel und MappedByteBuffer von Java NIO führt höchstwahrscheinlich zu einer Lösung, die viel schneller ist als jede Lösung mit FileInputStream. Grundsätzlich können Sie große Dateien im Speicher abbilden und direkte Puffer für kleine verwenden.

Alexander
quelle
4

In der Quelle von BufferedInputStream finden Sie: private static int DEFAULT_BUFFER_SIZE = 8192;
Es ist also in Ordnung, diesen Standardwert zu verwenden.
Wenn Sie jedoch weitere Informationen finden, erhalten Sie wertvollere Antworten.
Beispielsweise bevorzugt Ihre ADSL möglicherweise einen Puffer von 1454 Byte, was an der Nutzlast von TCP / IP liegt. Für Datenträger können Sie einen Wert verwenden, der der Blockgröße Ihres Datenträgers entspricht.

GoForce5500
quelle
1

Verwenden Sie BufferedInputStreams, wie bereits in anderen Antworten erwähnt.

Danach spielt die Puffergröße wohl keine Rolle mehr. Entweder ist das Programm an E / A gebunden, und eine Vergrößerung der Puffergröße gegenüber dem BIS-Standard hat keinen großen Einfluss auf die Leistung.

Oder das Programm ist in MessageDigest.update () CPU-gebunden, und die meiste Zeit wird nicht im Anwendungscode verbracht, sodass das Optimieren nicht hilft.

(Hmm ... bei mehreren Kernen könnten Threads helfen.)

Maglob
quelle
0

1024 ist für eine Vielzahl von Umständen geeignet, obwohl Sie in der Praxis möglicherweise eine bessere Leistung bei einer größeren oder kleineren Puffergröße sehen.

Dies würde von einer Reihe von Faktoren abhängen, einschließlich der Blockgröße des Dateisystems und der CPU-Hardware.

Es ist auch üblich, eine Potenz von 2 für die Puffergröße zu wählen, da die meiste zugrunde liegende Hardware mit Fle-Block- und Cache-Größen strukturiert ist, die eine Potenz von 2 sind. Mit den gepufferten Klassen können Sie die Puffergröße im Konstruktor angeben. Wenn keine angegeben ist, verwenden sie einen Standardwert, der in den meisten JVMs eine Zweierpotenz ist.

Unabhängig davon, welche Puffergröße Sie auswählen, ist die größte Leistungssteigerung der Übergang vom ungepufferten zum gepufferten Dateizugriff. Das Anpassen der Puffergröße kann die Leistung geringfügig verbessern. Wenn Sie jedoch keine extrem kleine oder extrem große Puffergröße verwenden, ist es unwahrscheinlich, dass dies erhebliche Auswirkungen hat.

Adrian Krebs
quelle