web-dev-qa-db-fra.com

Pourquoi "slurping" un fichier n'est pas une bonne pratique?

Pourquoi le "slurping" n'est-il pas une bonne pratique pour les E/S normales de fichier texte, et quand est-il utile?

Par exemple, pourquoi ne devrais-je pas les utiliser?

File.read('/path/to/text.txt').lines.each do |line|
  # do something with a line
end

ou

File.readlines('/path/to/text.txt').each do |line|
  # do something with a line
end
29
the Tin Man

Nous voyons encore et encore des questions concernant la lecture d'un fichier texte pour le traiter ligne par ligne, qui utilise des variantes de read ou readlines, qui enregistrent le fichier entier en mémoire en une seule action. 

La documentation de read dit:

Ouvre le fichier, recherche éventuellement le décalage donné, puis renvoie des octets de longueur (par défaut, le reste du fichier). [...]

La documentation de readlines dit:

Lit le fichier entier spécifié par son nom en tant que lignes individuelles et renvoie ces lignes dans un tableau. [...]

Arriver dans un petit fichier n'a pas d'importance, mais il arrive un moment où la mémoire doit être réorganisée au fur et à mesure que la mémoire tampon des données entrantes augmente, ce qui consomme du temps processeur. De plus, si les données occupent trop d’espace, le système d’exploitation doit s’impliquer uniquement pour que le script continue de fonctionner et commence à se mettre en file d’enregistrement sur disque. Sur un HTTPd (hôte Web) ou quelque chose nécessitant une réponse rapide, cela paralysera toute l'application.

Le slurping est généralement basé sur une incompréhension de la vitesse des entrées/sorties de fichiers ou sur l'idée qu'il est préférable de lire puis de scinder le tampon plutôt que de le lire ligne par ligne.

Voici un code de test pour illustrer le problème provoqué par "slurping". 

Enregistrez ceci sous "test.sh":

echo Building test files...

yes "abcdefghijklmnopqrstuvwxyz 123456890" | head -c 1000       > kb.txt
yes "abcdefghijklmnopqrstuvwxyz 123456890" | head -c 1000000    > mb.txt
yes "abcdefghijklmnopqrstuvwxyz 123456890" | head -c 1000000000 > gb1.txt
cat gb1.txt gb1.txt > gb2.txt
cat gb1.txt gb2.txt > gb3.txt

echo Testing...

Ruby -v

echo
for i in kb.txt mb.txt gb1.txt gb2.txt gb3.txt
do
  echo
  echo "Running: time Ruby readlines.rb $i"
  time Ruby readlines.rb $i
  echo '---------------------------------------'
  echo "Running: time Ruby foreach.rb $i"
  time Ruby foreach.rb $i
  echo
done

rm [km]b.txt gb[123].txt 

Il crée cinq fichiers de taille croissante. Les fichiers 1K sont faciles à traiter et sont très courants. Auparavant, les fichiers de 1 Mo étaient considérés comme volumineux, mais ils sont courants maintenant. 1 Go est courant dans mon environnement, et les fichiers dépassant 10 Go sont rencontrés périodiquement. Il est donc très important de savoir ce qui se passe à 1 Go et au-delà.

Enregistrez ceci sous "readlines.rb". Il ne fait rien sauf lire le fichier entier ligne par ligne en interne, et l'ajouter à un tableau qui est ensuite renvoyé, et semble être rapide puisque tout est écrit en C:

lines = File.readlines(ARGV.shift).size
puts "#{ lines } lines read"

Enregistrez ceci sous "foreach.rb":

lines = 0
File.foreach(ARGV.shift) { |l| lines += 1 }
puts "#{ lines } lines read"

En exécutant sh ./test.sh sur mon ordinateur portable, je reçois:

Building test files...
Testing...
Ruby 2.1.2p95 (2014-05-08 revision 45877) [x86_64-darwin13.0]

Lecture du fichier 1K:

Running: time Ruby readlines.rb kb.txt
28 lines read

real    0m0.998s
user    0m0.386s
sys 0m0.594s
---------------------------------------
Running: time Ruby foreach.rb kb.txt
28 lines read

real    0m1.019s
user    0m0.395s
sys 0m0.616s

Lecture du fichier de 1 Mo:

Running: time Ruby readlines.rb mb.txt
27028 lines read

real    0m1.021s
user    0m0.398s
sys 0m0.611s
---------------------------------------
Running: time Ruby foreach.rb mb.txt
27028 lines read

real    0m0.990s
user    0m0.391s
sys 0m0.591s

Lecture du fichier de 1 Go:

Running: time Ruby readlines.rb gb1.txt
27027028 lines read

real    0m19.407s
user    0m17.134s
sys 0m2.262s
---------------------------------------
Running: time Ruby foreach.rb gb1.txt
27027028 lines read

real    0m10.378s
user    0m9.472s
sys 0m0.898s

Lecture du fichier de 2 Go:

Running: time Ruby readlines.rb gb2.txt
54054055 lines read

real    0m58.904s
user    0m54.718s
sys 0m4.029s
---------------------------------------
Running: time Ruby foreach.rb gb2.txt
54054055 lines read

real    0m19.992s
user    0m18.765s
sys 0m1.194s

Lecture du fichier de 3 Go:

Running: time Ruby readlines.rb gb3.txt
81081082 lines read

real    2m7.260s
user    1m57.410s
sys 0m7.007s
---------------------------------------
Running: time Ruby foreach.rb gb3.txt
81081082 lines read

real    0m33.116s
user    0m30.790s
sys 0m2.134s

Notez que readlines est deux fois plus lent chaque fois que la taille du fichier augmente, et que foreach ralentit de manière linéaire. À 1Mo, nous pouvons voir que quelque chose affecte les E/S "slurping" qui n'affecte pas la lecture ligne par ligne. Et, comme les fichiers de 1 Mo sont très courants de nos jours, il est facile de voir qu'ils ralentiront le traitement des fichiers pendant la durée de vie d'un programme si nous ne pensons pas à l'avenir. Quelques secondes ici ou il n'y en a pas beaucoup lorsqu'elles se produisent une fois, mais si elles se produisent plusieurs fois par minute, l'impact sur les performances s'en ressent sérieusement d'ici la fin de l'année.

J'ai rencontré ce problème il y a des années lors du traitement de gros fichiers de données. Le code Perl que j’utilisais s’arrêtait périodiquement car il réaffectait de la mémoire lors du chargement du fichier. Réécrire le code pour ne pas supprimer le fichier de données, mais plutôt le lire et le traiter ligne par ligne, a permis d'améliorer considérablement la vitesse de lecture, passant de moins de cinq minutes à moins d'une unité et m'a appris une grande leçon.

"extraire" un fichier est parfois utile, en particulier si vous devez faire quelque chose au-delà des limites d'une ligne. Cependant, il est utile de réfléchir à d'autres moyens de lire un fichier si vous devez le faire. Par exemple, envisagez de conserver un petit tampon construit à partir des "n" dernières lignes et analysez-le. Cela évitera les problèmes de gestion de la mémoire causés par la tentative de lecture et de conservation du fichier entier. Ceci est discuté dans un blog relatif à Perl " Perl Slurp-Eaze " qui couvre les "whens" et les "pourquoi" pour justifier l'utilisation de lectures complètes de fichiers, et s'applique bien à Ruby.

Pour d’autres excellentes raisons de ne pas "glisser" vos fichiers, lisez " Comment rechercher du texte dans un fichier pour un motif et le remplacer par une valeur donnée ".

74
the Tin Man

Pourquoi le "slurping" n'est-il pas une bonne pratique pour les E/S de fichier texte normales

Le Tin Man frappe à droite. J'aimerais aussi ajouter: 

  • Dans de nombreux cas, la lecture du fichier entier en mémoire n’est pas traitable (car le fichier est trop volumineux ou les manipulations de chaîne disposent d’un espace O() exponentiel).

  • Souvent, vous ne pouvez pas anticiper la taille du fichier (cas particulier de ci-dessus)

  • Vous devez toujours essayer de connaître l'utilisation de la mémoire, et lire tout le fichier à la fois (même dans des situations triviales) n'est pas une bonne pratique s'il existe une option alternative (par exemple, ligne par ligne). Je sais par expérience que VBS est horrible en ce sens et que l’on est obligé de manipuler des fichiers via la ligne de commande. 

Ce concept s'applique non seulement aux fichiers, mais à tout autre processus dans lequel la taille de votre mémoire augmente rapidement et vous devez gérer chaque itération (ou ligne) à la fois. Fonctions du générateur vous aider en gérant le processus ou la lecture ligne par ligne afin de ne pas utiliser toutes les données en mémoire.

De plus, Python est très intelligent dans la lecture de fichiers in et sa méthode open() est conçue pour lire ligne par ligne par défaut. Voir " Améliorer votre Python:" Rendement "et explication des générateurs " qui explique un bon exemple de cas d'utilisation des fonctions de générateur.

3
Max Alcala

C'est un peu vieux, mais je suis un peu surpris que personne ne mentionne que l'extraction d'un fichier d'entrée rend le programme pratiquement inutile pour les pipelines. Dans un pipeline, le fichier d'entrée peut être petit mais lent. Si votre programme est en train de s’écrouler, cela signifie qu’il ne fonctionne pas avec les données au fur et à mesure de leur disponibilité, mais vous oblige plutôt à attendre le temps que cela prendra pour que l’entrée soit complète. Combien de temps? Cela peut être n'importe quoi, comme des heures ou des jours, plus ou moins, si je fais un grep ou un find dans une grande hiérarchie. Il pourrait également être conçu pour ne pas terminer, comme un fichier infini. Par exemple, journalctl -f continuera à générer tous les événements se produisant dans le système sans s’arrêter; tshark affichera tout ce qui se passe sur le réseau sans s’arrêter; ping continuera à faire un ping sans s'arrêter. /dev/zero est infini, /dev/urandom est infini.

La seule fois où je pouvais voir que le slurping était acceptable serait peut-être dans les fichiers de configuration, car le programme n'est probablement pas capable de faire quoi que ce soit de toute façon jusqu'à ce qu'il ait fini de lire cela.

1
JoL