Pourquoi la lecture de FileInputStream est-elle plus lente avec un tableau plus grand?

Si je lis des octets d’un fichier dans un octet [], je constate que les performances de FileInputStream se dégradent lorsque le tableau mesure environ 1 Mo, contre 128 Ko. Sur les 2 postes de travail que j’ai testés, il est presque deux fois plus rapide avec 128 Ko. Pourquoi donc?

import java.io.*; public class ReadFileInChuncks { public static void main(Ssortingng[] args) throws IOException { byte[] buffer1 = new byte[1024*128]; byte[] buffer2 = new byte[1024*1024]; Ssortingng 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(Ssortingng 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; } } 

les sorties

 422 ms 717 ms 422 ms 718 ms 

Notez qu’il s’agit d’une redéfinition d’une question déjà postée. L’autre a été pollué par des discussions indépendantes. Je vais marquer l’autre pour suppression.

Edit: Dupliquer, vraiment? Je suis sûr que je pourrais faire un meilleur code pour prouver mon point, mais cela ne répond pas à ma question

Edit2: J’ai exécuté le test avec chaque tampon entre 5 Ko et 1 000 Ko sur
Win7 / JRE 1.8.0_25 et la mauvaise performance commence à 508 KB et tous les suivants. Désolé pour les légions de diagramme erronées, x correspond à la taille du tampon, y correspond aux millisecondes.

entrez la description de l'image ici

TL; DR La baisse des performances est provoquée par l’allocation de mémoire, et non par des problèmes de lecture de fichier.

Un problème d’parsing comparative typique: vous comparez une chose, mais vous en mesurez une autre.

Tout d’abord, lorsque j’ai réécrit le code exemple à l’aide de RandomAccessFile , FileChannel et ByteBuffer.allocateDirect , le seuil a disparu. Les performances de lecture de fichier sont à peu près identiques pour les mémoires tampon 128K et 1M.

Contrairement aux E / S FileInputStream.read directes, FileInputStream.read ne peut pas charger de données directement dans un tableau d’octets Java. Il doit d’abord obtenir des données dans un tampon natif, puis les copier en Java à l’aide de la fonction JNI SetByteArrayRegion .

Nous devons donc regarder l’implémentation native de FileInputStream.read . Cela revient au morceau de code suivant dans 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; } 

Ici BUF_SIZE == 8192. Si le tampon est plus grand que cette zone de stack réservée, un tampon temporaire est alloué par malloc . Sous Windows, malloc est généralement implémenté via l’appel HeapAlloc WINAPI.

Ensuite, j’ai mesuré les performances des HeapAlloc + HeapFree seuls sans les entrées / sorties de fichiers. Les résultats étaient intéressants:

  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 

Comme vous pouvez le constater, les performances de l’allocation de mémoire du système d’exploitation changent radicalement à une limite de 1 Mo. Cela peut s'expliquer par différents algorithmes d'allocation utilisés pour les petits morceaux et pour les gros morceaux.

METTRE À JOUR

La documentation de HeapCreate confirme l'idée d'une stratégie d'allocation spécifique pour les blocs supérieurs à 1 Mo (voir la description de dwMaximumSize ).

En outre, le plus grand bloc de mémoire pouvant être alloué à partir du segment de mémoire est légèrement inférieur à 512 Ko pour un processus 32 bits et légèrement inférieur à 1 024 Ko pour un processus 64 bits.

...

Les demandes d'allocation de blocs de mémoire plus grands que la limite d'un segment de taille fixe n'échouent pas automatiquement. Au lieu de cela, le système appelle la fonction VirtualAlloc pour obtenir la mémoire nécessaire aux grands blocs.

La taille optimale de la mémoire tampon dépend de la taille des blocs du système de fichiers, de la taille du cache de la CPU et de la latence du cache. La plupart des ordinateurs utilisent la taille de bloc 4096 ou 8192, il est donc recommandé d’utiliser un tampon avec cette taille ou cette multiplicité de cette valeur.

J’ai réécrit le test pour tester différentes tailles de mémoire tampon.

Voici le nouveau code:

 public class ReadFileInChunks { public static void main(Ssortingng[] args) throws IOException { Ssortingng 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; } } 

Et voici les résultats...

 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 

Il semble qu'une sorte de seuil soit atteint à environ 1021 Ko de mémoire tampon. En regardant plus profondément dans ce que je vois ...

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

Il semble donc que l’effet de doubler se produit lorsque ce seuil est atteint. Je pensais au départ que la boucle while reliait readToArray doublait le nombre de fois où le seuil était atteint, mais ce n'est pas le cas, la boucle tant ne parcourt qu'une seule itération, que ce soit à 300 ms ou à 600 ms. Regardons donc le io_utils.c réel qui implémente lit les données du disque à la recherche d'indices.

 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; } 

Une chose à noter est que BUF_SIZE est défini sur 8192. L'effet de doublage se produit bien au-dessus de cela. Donc, le prochain coupable serait la méthode IO_Read .

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

Nous allons donc à la méthode handleRead.

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

Cette méthode remet la demande à une méthode appelée 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; } 

Et c’est là que la piste est froide… pour le moment. Si je trouve le code pour ReadFile, je jetterai un œil et posterai.

Cela peut être dû au cache cpu,

cpu a sa propre mémoire cache et il existe une taille de correction pour laquelle vous pouvez vérifier la taille de votre cache cpu en exécutant cette commande sur cmd

wmic cpu obtenir L2CacheSize

Supposons que la taille du cache cpu soit de 256 Ko. Par conséquent, si vous lisez des fragments de 256 Ko ou moins, le contenu écrit dans la mémoire tampon est toujours dans le cache de la CPU lorsque la lecture y accède. Si vous avez des blocs supérieurs à 256 Ko, les 256 derniers Ko lus se trouvent dans le cache de la CPU. Ainsi, lorsque la lecture commence au début, le contenu doit être extrait de la mémoire principale.