Pourquoi l'ouverture d'un fichier est-elle plus rapide que la lecture d'un contenu variable?

36

Dans un bashscript, j'ai besoin de différentes valeurs provenant de /proc/fichiers. Jusqu'à présent, j'ai des dizaines de lignes qui attrapent les fichiers directement comme ça:

grep -oP '^MemFree: *\K[0-9]+' /proc/meminfo

Afin de rendre cela plus efficace, j’ai sauvegardé le contenu du fichier dans une variable et ai ajouté que:

a=$(</proc/meminfo)
echo "$a" | grep -oP '^MemFree: *\K[0-9]+'

Au lieu d'ouvrir le fichier plusieurs fois, cela devrait simplement l'ouvrir une fois et grep le contenu variable, ce qui, je suppose, serait plus rapide - mais en réalité, il est plus lent:

bash 4.4.19 $ time for i in {1..1000};do grep ^MemFree /proc/meminfo;done >/dev/null
real    0m0.803s
user    0m0.619s
sys     0m0.232s
bash 4.4.19 $ a=$(</proc/meminfo)
bash 4.4.19 $ time for i in {1..1000};do echo "$a"|grep ^MemFree; done >/dev/null
real    0m1.182s
user    0m1.425s
sys     0m0.506s

La même chose est vraie pour dashet zsh. Je soupçonnais l’état particulier des /proc/fichiers comme raison, mais lorsque je copie le contenu de /proc/meminfodans un fichier normal et que je l’ utilise, les résultats sont les mêmes:

bash 4.4.19 $ cat </proc/meminfo >meminfo
bash 4.4.19 $ time for i in $(seq 1 1000);do grep ^MemFree meminfo; done >/dev/null
real    0m0.790s
user    0m0.608s
sys     0m0.227s

L'utilisation d'une chaîne here pour enregistrer le canal le rend légèrement plus rapide, mais pas aussi rapide qu'avec les fichiers:

bash 4.4.19 $ time for i in $(seq 1 1000);do <<<"$a" grep ^MemFree; done >/dev/null
real    0m0.977s
user    0m0.758s
sys     0m0.268s

Pourquoi l'ouverture d'un fichier est-elle plus rapide que la lecture du même contenu à partir d'une variable?

dessert
la source
@ l0b0 Cette hypothèse n'est pas erronée, la question montre comment je l'ai trouvée et les réponses expliquent pourquoi c'est le cas. Votre édition fait maintenant les réponses qui ne répondent plus à la question du titre: elles ne disent pas si c'est le cas.
dessert le
OK, clarifié. Parce que l'en-tête était mauvais dans la grande majorité des cas, pas pour certains fichiers spéciaux mappés en mémoire.
l0b0
@ l0b0 Non, c'est ce que je demande ici: «Je pensais que l'état spécial des /proc/fichiers était une raison, mais lorsque je copie le contenu de /proc/meminfodans un fichier normal et que les résultats sont les mêmes:" Cela n'a rien de spécial /proc/fichiers, la lecture de fichiers normaux est également plus rapide!
dessert le

Réponses:

47

Ici, il ne s’agit pas d’ ouvrir un fichier ou de lire le contenu d’une variable, mais plutôt de forger un processus supplémentaire ou non.

grep -oP '^MemFree: *\K[0-9]+' /proc/meminfoforks un processus qui s'exécute et grepqui s'ouvre /proc/meminfo(un fichier virtuel, en mémoire, aucune entrée / sortie de disque impliquée) le lit et correspond à l'expression rationnelle.

La partie la plus coûteuse consiste à graver le processus et à charger l'utilitaire grep et ses dépendances de bibliothèque, à effectuer la liaison dynamique, à ouvrir la base de données de paramètres régionaux, à des dizaines de fichiers stockés sur le disque (mais probablement mis en cache en mémoire).

La partie sur la lecture /proc/meminfoest insignifiante en comparaison, le noyau a besoin de peu de temps pour générer les informations qu'il grepcontient et peu de temps pour le lire.

Si vous utilisez strace -ccette fonction, vous verrez que les appels système à open()un read()système utilisés pour lire /proc/meminfosont des cacahuètes comparés à tout ce qui se grepfait pour démarrer ( strace -cne compte pas le bifurquage).

Dans:

a=$(</proc/meminfo)

Dans la plupart des shells qui supportent cet $(<...)opérateur ksh, le shell ouvre simplement le fichier et lit son contenu (et supprime les caractères de fin de ligne suivants). bashest différent et beaucoup moins efficace dans la mesure où il crée un processus permettant cette lecture et transmet les données au parent via un canal. Mais ici, c'est fait une fois, donc ce n'est pas grave.

Dans:

printf '%s\n' "$a" | grep '^MemFree'

Le shell doit générer deux processus, qui s'exécutent simultanément mais interagissent via un canal. La création de cette pipe, sa démolition, son écriture et sa lecture ont un coût peu élevé. Le coût beaucoup plus élevé est la génération d'un processus supplémentaire. La planification des processus a également un impact.

Vous constaterez peut-être que l'utilisation de l' <<<opérateur zsh accélère légèrement:

grep '^MemFree' <<< "$a"

En zsh et bash, cela se fait en écrivant le contenu de $adans un fichier temporaire, ce qui est moins coûteux que de générer un processus supplémentaire, mais ne vous apportera probablement aucun gain par rapport à la récupération immédiate des données /proc/meminfo. C'est toujours moins efficace que votre approche qui copie /proc/meminfosur disque, car l'écriture du fichier temporaire est faite à chaque itération.

dashne supporte pas here-strings, mais ses heredocs sont implémentés avec un tube ne générant pas de processus supplémentaire. Dans:

 grep '^MemFree' << EOF
 $a
 EOF

Le shell crée un tuyau, lance un processus. L'enfant s'exécute grepavec son stdin comme extrémité de lecture du tuyau et le parent écrit le contenu à l'autre extrémité du tuyau.

Mais la gestion des canalisations et la synchronisation des processus risquent toujours d'être plus onéreuses que de simplement récupérer les données immédiatement /proc/meminfo.

Le contenu de /proc/meminfoest court et prend peu de temps à produire. Si vous souhaitez économiser certains cycles de traitement, vous souhaitez supprimer les parties coûteuses: forking des processus et exécution de commandes externes.

Comme:

IFS= read -rd '' meminfo < /proc/meminfo
memfree=${meminfo#*MemFree:}
memfree=${memfree%%$'\n'*}
memfree=${memfree#"${memfree%%[! ]*}"}

Evitez bashcependant les correspondances de motifs très peu efficaces. Avec zsh -o extendedglob, vous pouvez le raccourcir à:

memfree=${${"$(</proc/meminfo)"##*MemFree: #}%%$'\n'*}

Notez que ^c'est spécial dans beaucoup de coquillages (Bourne, fish, rc, es et zsh avec l'option extendedglob au moins), je vous recommande de le citer. Notez également que echovous ne pouvez pas utiliser pour produire des données arbitraires (d'où mon utilisation de printfci - dessus).

Stéphane Chazelas
la source
4
Dans le cas où printfvous dites, le shell doit générer deux processus, mais printfun shell n'est-il pas intégré?
David Conrad
6
@DavidConrad C'est le cas, mais la plupart des shells n'essayent pas d'analyser le pipeline pour savoir quelles pièces il pourrait exécuter dans le processus en cours. Il se bifurque et laisse les enfants le découvrir. Dans ce cas, le processus parent se divise deux fois; l'enfant du côté gauche voit alors un élément intégré et l'exécute; l'enfant pour le côté droit voit grepet exécute.
Chepner
1
@DavidConrad, le canal est un mécanisme IPC, de sorte que dans tous les cas, les deux parties devront s'exécuter dans des processus différents. Pendant que vous A | By êtes, il y a des shells tels que AT & T ksh ou zsh qui s'exécutent Bdans le processus shell en cours s'il s'agit d'une commande intégrée ou composée ou d'une commande de fonction. Je n'en connais aucun qui s'exécute Adans le processus en cours. Pour faire cela, ils devraient gérer SIGPIPE de manière complexe, comme si elle Afonctionnait dans un processus enfant et sans mettre fin au shell pour que le comportement ne soit pas trop surprenant lorsqu’il se termine Btôt. Il est beaucoup plus facile de lancer Ble processus parent.
Stéphane Chazelas
Bash soutient<<<
D. Ben Knoble
1
@ D.BenKnoble, je ne voulais pas dire que je ne bashsoutenais pas <<<, juste que l'opérateur venait de zshcomme $(<...)venait de ksh.
Stéphane Chazelas
6

Dans votre premier cas, vous utilisez simplement l'utilitaire grep et recherchez quelque chose dans un fichier /proc/meminfo. Il /procs'agit d'un système de fichiers virtuel. Le /proc/meminfofichier est donc dans la mémoire et il faut très peu de temps pour en extraire le contenu.

Mais dans le second cas, vous créez un canal, puis transmettez la sortie de la première commande à la deuxième commande utilisant ce canal, ce qui est coûteux.

La différence est due à /proc(car il est en mémoire) et pipe, voir l'exemple ci-dessous:

time for i in {1..1000};do grep ^MemFree /proc/meminfo;done >/dev/null

real    0m0.914s
user    0m0.032s
sys     0m0.148s


cat /proc/meminfo > file
time for i in {1..1000};do grep ^MemFree file;done >/dev/null

real    0m0.938s
user    0m0.032s
sys     0m0.152s


time for i in {1..1000};do echo "$a"|grep ^MemFree; done >/dev/null

real    0m1.016s
user    0m0.040s
sys     0m0.232s
Prvt_Yadav
la source
1

Vous appelez une commande externe dans les deux cas (grep). L'appel externe nécessite un sous-shell. Le fait de fourrer cette coquille est la cause fondamentale du retard. Les deux cas sont similaires, donc: un délai similaire.

Si vous voulez lire le fichier externe une seule fois et l'utiliser (à partir d'une variable) plusieurs fois, ne sortez pas du shell:

meminfo=$(< /dev/meminfo)    
time for i in {1..1000};do 
    [[ $meminfo =~ MemFree:\ *([0-9]*)\ *.B ]] 
    printf '%s\n' "${BASH_REMATCH[1]}"
done

Ce qui ne prend que 0,1 seconde environ au lieu de 1 seconde pour l'appel de grep.

Isaac
la source