2015-02-24 11 views
8

Wenn ich Bytes aus einer Datei in ein Byte [] lesen, sehe ich, dass FileInputStream Leistung schlechter, wenn das Array etwa 1 MB im Vergleich zu 128 KB ist. An den 2 von mir getesteten Arbeitsplätzen ist es mit 128 KB fast doppelt so schnell. Warum das?Warum ist FileInputStream lesen langsamer mit größeren Array

import java.io.*; 

public class ReadFileInChuncks 
{ 
    public static void main(String[] args) throws IOException 
    { 
     byte[] buffer1 = new byte[1024*128]; 
     byte[] buffer2 = new byte[1024*1024]; 

     String path = "some 1 gb big file"; 

     readFileInChuncks(path, buffer1, false); 

     readFileInChuncks(path, buffer1, true); 
     readFileInChuncks(path, buffer2, true); 
     readFileInChuncks(path, buffer1, true); 
     readFileInChuncks(path, buffer2, true); 
    } 

    public static void readFileInChuncks(String path, byte[] buffer, boolean report) throws IOException 
    { 
     long t = System.currentTimeMillis(); 

     InputStream is = new FileInputStream(path); 
     while ((readToArray(is, buffer)) != 0) {} 

     if (report) 
      System.out.println((System.currentTimeMillis()-t) + " ms"); 
    } 

    public static int readToArray(InputStream is, byte[] buffer) throws IOException 
    { 
     int index = 0; 
     while (index != buffer.length) 
     { 
      int read = is.read(buffer, index, buffer.length - index); 
      if (read == -1) 
       break; 
      index += read; 
     } 
     return index; 
    } 
} 

Ausgänge

422 ms 
717 ms 
422 ms 
718 ms 

Hinweis ist dies eine Neudefinition einer bereits gebucht Frage. Der andere war mit nicht verwandten Diskussionen belastet. Ich werde den anderen zur Löschung markieren.

Bearbeiten: Duplizieren, wirklich? Ich könnte sicher etwas besseren Code Beweis meines Punkt, aber this beantwortet meine Frage nicht

Edit2: Ich lief den Test mit jedem Puffer zwischen 5 KB und 1000 KB auf
Win7/JRE 1.8.0_25 und den schlechten Leistung beginnt bei Precis 508 KB und alle nachfolgenden. Sorry für die schlechte Diagramm Legion, x Puffergröße ist, y Millisekunden

enter image description here

+1

Ich habe den gleichen Code zweimal zu einer großen> 4GB hprof-Datei ausgeführt. und Ergebnisse gefunden, die dem entgegenstehen, was Sie erwähnt haben. 8358 ms 6302 ms 7986 ms 6256 ms und 8591 ms 6326 ms 8022 ms 6268 ms –

+0

Auf welchem ​​O/JRE-Combo ist das? – fge

+0

mac os x/jre 1.7 –

Antwort

8

TL; DR Der Leistungsabfall wird durch die Speicherzuordnung verursacht, nicht durch Dateileseprobleme.

Ein typisches Benchmark-Problem: Sie benchmarken eine Sache, messen aber tatsächlich eine andere.

Zuerst, wenn ich den Beispielcode mit RandomAccessFile, FileChannel und ByteBuffer.allocateDirect umschrieb, ist der Schwellenwert verschwunden. Die Dateileseleistung wurde für 128 K und 1 M Puffer ungefähr gleich.

Im Gegensatz zu direkten ByteBuffer I/O FileInputStream.read können Daten nicht direkt in Java-Byte-Array geladen werden. Es muss zuerst Daten in einen nativen Puffer laden und dann mithilfe der JNI-Funktion SetByteArrayRegion nach Java kopieren.

Also müssen wir uns die native Implementierung von FileInputStream.read ansehen. Es kommt auf das folgende Stück Code in io_util.c:

if (len == 0) { 
     return 0; 
    } else if (len > BUF_SIZE) { 
     buf = malloc(len); 
     if (buf == NULL) { 
      JNU_ThrowOutOfMemoryError(env, NULL); 
      return 0; 
     } 
    } else { 
     buf = stackBuf; 
    } 

Hier buf_size == 8192. Wenn der Puffer größer als dieser reservierten Stapelbereich, ein temporärer Puffer von malloc zugeordnet ist. Unter Windows malloc wird in der Regel über HeapAlloc WINAPI-Aufruf realisiert.

Als nächstes habe ich die Leistung von HeapAlloc + HeapFree Anrufe allein ohne Datei I/O gemessen. Die Ergebnisse waren interessant:

 128K: 5 μs 
    256K: 10 μs 
    384K: 15 μs 
    512K: 20 μs 
    640K: 25 μs 
    768K: 29 μs 
    896K: 33 μs 
    1024K: 316 μs <-- almost 10x leap 
    1152K: 356 μs 
    1280K: 399 μs 
    1408K: 436 μs 
    1536K: 474 μs 
    1664K: 511 μs 
    1792K: 553 μs 
    1920K: 592 μs 
    2048K: 628 μs 

Wie Sie sehen können, ändert sich die Leistung der OS-Speicherzuweisung drastisch bei 1MB Grenze. Dies kann durch verschiedene Zuweisungsalgorithmen erklärt werden, die für kleine Brocken und für große Brocken verwendet werden.

UPDATE

Die Dokumentation für HeapCreate für Blöcke größer als 1 MB, die Idee über spezifische Zuweisungsstrategie bestätigt (siehe dwMaximumSize Beschreibung).

Auch der größte Speicherblock, der aus dem Heap zugeordnet werden kann, ist etwas weniger als 512 KB für einen 32-Bit-Prozess und etwas weniger als 1.024 KB für einen 64-Bit-Prozess.

...

Anforderungen zum Zuweisen von Speicherblöcken, die größer als der Grenzwert für einen Heap mit fester Größe sind, schlagen nicht automatisch fehl; Stattdessen ruft das System die VirtualAlloc-Funktion auf, um den Speicher zu erhalten, der für große Blöcke benötigt wird.

+0

im letzten Kommentar zu der Frage @Robert erwähnte L2-Cache von 1024kb, glaubst du, dass es der Grund hinter dem Malloc-Sprung sein kann? – Zielu

+0

@Zielu Nein, ich bin absolut sicher, dass es nicht der Grund für 10x Unterschied sein kann. Tatsächlich wird der zugewiesene Speicher in meinem HeapAlloc-Test nicht einmal verwendet, so dass der Cache keine Rolle spielt. In der Tat habe ich schon den Grund gefunden, siehe das Update. – apangin

+0

BUFFER_SIZE ist 8192. Das Problem tritt nicht auf, bis der Puffer mehrfach größer ist, was bedeutet, dass malloc ohne Probleme verwendet wird. Fehle ich etwas? –

1

optimale Puffergröße depands auf Dateisystemblockgröße, CPU-Cache-Größe und die Cache-Latenz. Die meisten os'es verwenden Blockgröße 4096 oder 8192, daher wird empfohlen, einen Puffer mit dieser Größe oder Multiplizität dieses Werts zu verwenden.

+0

Außerdem gibt es typischerweise sehr wenig, wenn überhaupt, einen Gewinn von mehr als 4K. Das Erstellen von großen Puffern ist enorm verschwenderisch und kommt zurück, wenn Sie den Code plötzlich skalieren (und jetzt sind es 10.000 statt einer Handvoll). Standard-JVM-Puffer sind 128 Bytes, obwohl ich das etwas zu klein finde. –

+0

@Piotr Zych, Vielzahl von 4096 oder 8192 scheint nicht die Heilung zu sein. Sehen Sie sich das Diagramm in der Frage an. – Stig

+0

Ich nehme an, dass in diesem Fall die CPU-Cache-Größe das Hauptproblem sein kann. Der Hauptspeicher ist viel langsamer als der CPU-L2-Cache. – Robert

-1

Dies kann wegen der CPU-Cache sein,

CPU einen eigenen Cache-Speicher hat, und es gibt einige fix Größe für die Sie Ihre CPU-Cache-Größe überprüfen kann durch die Ausführung des Befehls auf cmd

wmic cpu erhalten L2CacheSize

nehme an, Sie 256k als CPU-Cache-Größe haben, Also, was passiert, ist, wenn Sie 256k Brocken lesen oder kleiner ist, der Inhalt, der in den Puffer geschrieben wurde, ist nach wie vor in der CPU-Cache, wenn die rea d greift darauf zu. Wenn Sie Chunks größer als 256k haben, befinden sich die letzten 256k, die gelesen wurden, im CPU-Cache. Wenn der Lesevorgang von Anfang an beginnt, muss der Inhalt aus dem Hauptspeicher abgerufen werden.

+0

Lesen Sie die Frage sorgfältig: "Win7/JRE 1.8.0_25 und die schlechte Leistung beginnt bei Precis 508 KB und alle nachfolgenden" – Zielu

0

Ich schrieb den Test neu, um verschiedene Puffergrößen zu testen. Hier

ist der neue Code:

public class ReadFileInChunks { 

    public static void main(String[] args) throws IOException { 
     String path = "C:\\\\tmp\\1GB.zip"; 
     readFileInChuncks(path, new byte[1024 * 128], false); 

     for (int i = 1; i <= 1024; i+=10) { 
      readFileInChuncks(path, new byte[1024 * i], true); 
     } 
    } 

    public static void readFileInChuncks(String path, byte[] buffer, boolean report) throws IOException { 
     long t = System.currentTimeMillis(); 

     InputStream is = new FileInputStream(path); 
     while ((readToArray(is, buffer)) != 0) { 
     } 

     if (report) { 
      System.out.println("buffer size = " + buffer.length/1024 + "kB , duration = " + (System.currentTimeMillis() - t) + " ms"); 
     } 
    } 

    public static int readToArray(InputStream is, byte[] buffer) throws IOException { 
     int index = 0; 
     while (index != buffer.length) { 
      int read = is.read(buffer, index, buffer.length - index); 
      if (read == -1) { 
       break; 
      } 
      index += read; 
     } 
     return index; 
    } 

} 

Und hier sind die Ergebnisse ...

buffer size = 121kB , duration = 320 ms 
buffer size = 131kB , duration = 330 ms 
buffer size = 141kB , duration = 330 ms 
buffer size = 151kB , duration = 323 ms 
buffer size = 161kB , duration = 320 ms 
buffer size = 171kB , duration = 320 ms 
buffer size = 181kB , duration = 320 ms 
buffer size = 191kB , duration = 310 ms 
buffer size = 201kB , duration = 320 ms 
buffer size = 211kB , duration = 310 ms 
buffer size = 221kB , duration = 310 ms 
buffer size = 231kB , duration = 310 ms 
buffer size = 241kB , duration = 310 ms 
buffer size = 251kB , duration = 310 ms 
buffer size = 261kB , duration = 320 ms 
buffer size = 271kB , duration = 310 ms 
buffer size = 281kB , duration = 320 ms 
buffer size = 291kB , duration = 310 ms 
buffer size = 301kB , duration = 319 ms 
buffer size = 311kB , duration = 320 ms 
buffer size = 321kB , duration = 310 ms 
buffer size = 331kB , duration = 320 ms 
buffer size = 341kB , duration = 310 ms 
buffer size = 351kB , duration = 320 ms 
buffer size = 361kB , duration = 310 ms 
buffer size = 371kB , duration = 320 ms 
buffer size = 381kB , duration = 311 ms 
buffer size = 391kB , duration = 310 ms 
buffer size = 401kB , duration = 310 ms 
buffer size = 411kB , duration = 320 ms 
buffer size = 421kB , duration = 310 ms 
buffer size = 431kB , duration = 310 ms 
buffer size = 441kB , duration = 310 ms 
buffer size = 451kB , duration = 320 ms 
buffer size = 461kB , duration = 310 ms 
buffer size = 471kB , duration = 310 ms 
buffer size = 481kB , duration = 310 ms 
buffer size = 491kB , duration = 310 ms 
buffer size = 501kB , duration = 310 ms 
buffer size = 511kB , duration = 320 ms 
buffer size = 521kB , duration = 300 ms 
buffer size = 531kB , duration = 310 ms 
buffer size = 541kB , duration = 312 ms 
buffer size = 551kB , duration = 311 ms 
buffer size = 561kB , duration = 320 ms 
buffer size = 571kB , duration = 310 ms 
buffer size = 581kB , duration = 314 ms 
buffer size = 591kB , duration = 320 ms 
buffer size = 601kB , duration = 310 ms 
buffer size = 611kB , duration = 310 ms 
buffer size = 621kB , duration = 310 ms 
buffer size = 631kB , duration = 310 ms 
buffer size = 641kB , duration = 310 ms 
buffer size = 651kB , duration = 310 ms 
buffer size = 661kB , duration = 301 ms 
buffer size = 671kB , duration = 310 ms 
buffer size = 681kB , duration = 310 ms 
buffer size = 691kB , duration = 310 ms 
buffer size = 701kB , duration = 310 ms 
buffer size = 711kB , duration = 300 ms 
buffer size = 721kB , duration = 310 ms 
buffer size = 731kB , duration = 310 ms 
buffer size = 741kB , duration = 310 ms 
buffer size = 751kB , duration = 310 ms 
buffer size = 761kB , duration = 311 ms 
buffer size = 771kB , duration = 310 ms 
buffer size = 781kB , duration = 300 ms 
buffer size = 791kB , duration = 300 ms 
buffer size = 801kB , duration = 310 ms 
buffer size = 811kB , duration = 310 ms 
buffer size = 821kB , duration = 300 ms 
buffer size = 831kB , duration = 310 ms 
buffer size = 841kB , duration = 310 ms 
buffer size = 851kB , duration = 300 ms 
buffer size = 861kB , duration = 310 ms 
buffer size = 871kB , duration = 310 ms 
buffer size = 881kB , duration = 310 ms 
buffer size = 891kB , duration = 304 ms 
buffer size = 901kB , duration = 310 ms 
buffer size = 911kB , duration = 310 ms 
buffer size = 921kB , duration = 310 ms 
buffer size = 931kB , duration = 299 ms 
buffer size = 941kB , duration = 321 ms 
buffer size = 951kB , duration = 310 ms 
buffer size = 961kB , duration = 310 ms 
buffer size = 971kB , duration = 310 ms 
buffer size = 981kB , duration = 310 ms 
buffer size = 991kB , duration = 295 ms 
buffer size = 1001kB , duration = 339 ms 
buffer size = 1011kB , duration = 302 ms 
buffer size = 1021kB , duration = 610 ms 

Es sieht aus wie eine Art Schwelle getroffen wird auf rund 1021kB Größe Puffer. Suchen Sie diese tiefer in Verstehe ...

buffer size = 1017kB , duration = 310 ms 
buffer size = 1018kB , duration = 310 ms 
buffer size = 1019kB , duration = 602 ms 
buffer size = 1020kB , duration = 600 ms 

So sieht es aus wie es irgendeine Art von Wirkung zu verdoppeln ist sich geht, wenn diese Schwelle getroffen wird. Meine anfänglichen Gedanken sind, dass die readToArray while-Schleife die doppelte Anzahl von Malen wiederholte, als der Schwellenwert erreicht wurde, aber das ist nicht der Fall, die while-Schleife durchläuft nur eine Iteration, egal ob 300ms oder 600ms laufen. Sehen wir uns also die tatsächliche io_utils.c an, die die Daten tatsächlich von der Festplatte liest, um einige Hinweise zu erhalten.

jint 
readBytes(JNIEnv *env, jobject this, jbyteArray bytes, 
      jint off, jint len, jfieldID fid) 
{ 
    jint nread; 
    char stackBuf[BUF_SIZE]; 
    char *buf = NULL; 
    FD fd; 

    if (IS_NULL(bytes)) { 
     JNU_ThrowNullPointerException(env, NULL); 
     return -1; 
    } 

    if (outOfBounds(env, off, len, bytes)) { 
     JNU_ThrowByName(env, "java/lang/IndexOutOfBoundsException", NULL); 
     return -1; 
    } 

    if (len == 0) { 
     return 0; 
    } else if (len > BUF_SIZE) { 
     buf = malloc(len); 
     if (buf == NULL) { 
      JNU_ThrowOutOfMemoryError(env, NULL); 
      return 0; 
     } 
    } else { 
     buf = stackBuf; 
    } 

    fd = GET_FD(this, fid); 
    if (fd == -1) { 
     JNU_ThrowIOException(env, "Stream Closed"); 
     nread = -1; 
    } else { 
     nread = (jint)IO_Read(fd, buf, len); 
     if (nread > 0) { 
      (*env)->SetByteArrayRegion(env, bytes, off, nread, (jbyte *)buf); 
     } else if (nread == JVM_IO_ERR) { 
      JNU_ThrowIOExceptionWithLastError(env, "Read error"); 
     } else if (nread == JVM_IO_INTR) { 
      JNU_ThrowByName(env, "java/io/InterruptedIOException", NULL); 
     } else { /* EOF */ 
      nread = -1; 
     } 
    } 

    if (buf != stackBuf) { 
     free(buf); 
    } 
    return nread; 
} 

Eine Sache zu beachten ist, dass buf_size auf 8192 gesetzt ist die Verdoppelung Effekt geschieht über auf diese Weise. Der nächste Schuldige wäre also die IO_Read Methode.

windows/native/java/io/io_util_md.h:#define IO_Read handleRead 

So gehen wir zu handleRead Methode.

windows/native/java/io/io_util_md.c:handleRead(jlong fd, void *buf, jint len) 

Diese Methode Hände der Anfrage an eine Methode namens ReadFile.

JNIEXPORT 
size_t 
handleRead(jlong fd, void *buf, jint len) 
{ 
    DWORD read = 0; 
    BOOL result = 0; 
    HANDLE h = (HANDLE)fd; 
    if (h == INVALID_HANDLE_VALUE) { 
     return -1; 
    } 
    result = ReadFile(h,   /* File handle to read */ 
         buf,  /* address to put data */ 
         len,  /* number of bytes to read */ 
         &read,  /* number of bytes read */ 
         NULL);  /* no overlapped struct */ 
    if (result == 0) { 
     int error = GetLastError(); 
     if (error == ERROR_BROKEN_PIPE) { 
      return 0; /* EOF */ 
     } 
     return -1; 
    } 
    return read; 
} 

Und hier läuft die Spur kalt .... für jetzt. Wenn ich den Code für ReadFile finde, werde ich einen Blick darauf werfen und zurück posten.

+0

Alles sehr interessant, aber das ist keine Antwort. Es sollte in deine Frage bearbeitet worden sein. – EJP

+2

@EJP als der Autor sagte, es ist in Arbeit und es scheint ein guter zu sein, da eine weitere neue Antwort darauf aufbaut. Diese Frage konnte nicht innerhalb von 5 Minuten beantwortet werden, daher sollte es meines Erachtens erlaubt sein, seine Untersuchung zu teilen, solange es Sinn macht und dies tut. – Zielu

+1

@Jose danke für die Mühe. Sie waren der Erste, der das Problem aus dem richtigen Blickwinkel ansprach. Wenn ich das Kopfgeld teilen könnte, würde ich es tun, aber es gibt keine solche Option und Apangin gelang es, dem Grund auf den Grund zu gehen. – Zielu

Verwandte Themen