Pourquoi y a-t-il une telle différence dans le temps d'exécution de l'écho et du chat?

15

Répondre à cette question m'a amené à poser une autre question:
je pensais que les scripts suivants font la même chose et le second devrait être beaucoup plus rapide, car le premier utilise catqui a besoin d'ouvrir le fichier encore et encore mais le second ouvre le fichier uniquement une fois, puis fait écho à une variable:

(Voir la section de mise à jour pour le code correct.)

Premier:

#!/bin/sh
for j in seq 10; do
  cat input
done >> output

Seconde:

#!/bin/sh
i=`cat input`
for j in seq 10; do
  echo $i
done >> output

tandis que l'entrée est d'environ 50 mégaoctets.

Mais quand j'ai essayé le second, c'était trop, trop lent car faire écho à la variable iétait un processus énorme. J'ai également eu quelques problèmes avec le deuxième script, par exemple la taille du fichier de sortie était inférieure à celle attendue.

J'ai également vérifié la page de manuel de echoet catpour les comparer:

echo - affiche une ligne de texte

cat - concaténer des fichiers et imprimer sur la sortie standard

Mais je n'ai pas compris la différence.

Donc:

  • Pourquoi le chat est si rapide et l'écho est si lent dans le deuxième script?
  • Ou est le problème avec la variable i? (parce que dans la page de manuel, echoil est dit qu'il affiche "une ligne de texte" et donc je suppose qu'il est optimisé uniquement pour les variables courtes, pas pour les variables très très longues comme i. Cependant, ce n'est qu'une supposition.)
  • Et pourquoi j'ai des problèmes lorsque j'utilise echo?

MISE À JOUR

J'ai utilisé seq 10au lieu de `seq 10`mal. Ceci est du code édité:

Premier:

#!/bin/sh
for j in `seq 10`; do
  cat input
done >> output

Seconde:

#!/bin/sh
i=`cat input`
for j in `seq 10`; do
  echo $i
done >> output

(Un merci spécial à roaima .)

Cependant, ce n'est pas le but du problème. Même si la boucle ne se produit qu'une seule fois, j'obtiens le même problème: catfonctionne beaucoup plus rapidement que echo.

Mohammad
la source
1
et qu'en est-il cat $(for i in $(seq 1 10); do echo "input"; done) >> output? :)
netmonk
2
C'est echoplus rapide. Ce qui vous manque, c'est que vous faites trop travailler le shell en ne citant pas les variables lorsque vous les utilisez.
roaima
Citer les variables n'est pas le problème; le problème est la variable i elle-même (c'est-à-dire l'utiliser comme une étape intermédiaire entre l'entrée et la sortie).
Aleksander
`echo $ i` - ne faites pas ça. Utilisez printf et citez l'argument.
PSkocik
1
@PSkocik Ce que je dis, c'est que vous voulez printf '%s' "$i", non echo $i. @cuonglm explique bien certains des problèmes d'écho dans sa réponse. Pour savoir pourquoi même citer n'est pas suffisant dans certains cas avec écho, voir unix.stackexchange.com/questions/65803/…
PSkocik

Réponses:

24

Il y a plusieurs choses à considérer ici.

i=`cat input`

peut être cher et il y a beaucoup de variations entre les coques.

C'est une fonctionnalité appelée substitution de commandes. L'idée est de stocker la sortie entière de la commande moins les caractères de fin de ligne dans la ivariable en mémoire.

Pour ce faire, les shells forkent la commande dans un sous-shell et lisent sa sortie via un tube ou une paire de sockets. Vous voyez beaucoup de variations ici. Sur un fichier de 50 Mo ici, je peux voir par exemple que bash est 6 fois plus lent que ksh93 mais légèrement plus rapide que zsh et deux fois plus rapide que yash.

La principale raison d' bashêtre lent est qu'il lit à partir du canal 128 octets à la fois (tandis que d'autres shells lisent 4KiB ou 8KiB à la fois) et est pénalisé par la surcharge des appels système.

zshdoit effectuer un post-traitement pour échapper aux octets NUL (d'autres shells se brisent sur les octets NUL), et yasheffectue un traitement encore plus intensif en analysant les caractères multi-octets.

Tous les shells doivent supprimer les caractères de fin de ligne qu'ils peuvent faire plus ou moins efficacement.

Certains peuvent vouloir gérer les octets NUL plus gracieusement que d'autres et vérifier leur présence.

Ensuite, une fois que vous avez cette grande variable en mémoire, toute manipulation implique généralement d'allouer plus de mémoire et de copier les données.

Ici, vous passez (vouliez passer) le contenu de la variable à echo.

Heureusement, echoest intégré dans votre shell, sinon l'exécution aurait probablement échoué avec une erreur de liste d'arguments trop longue . Même dans ce cas, la construction du tableau de la liste des arguments impliquera éventuellement la copie du contenu de la variable.

L'autre problème principal dans votre approche de substitution de commandes est que vous invoquez l' opérateur split + glob (en oubliant de citer la variable).

Pour cela, les shells doivent traiter la chaîne comme une chaîne de caractères (bien que certains shells ne le soient pas et soient bogués à cet égard), donc dans les environnements locaux UTF-8, cela signifie analyser les séquences UTF-8 (si ce n'est déjà fait comme le yashfait le cas) , recherchez les $IFScaractères dans la chaîne. S'il $IFScontient de l'espace, une tabulation ou une nouvelle ligne (ce qui est le cas par défaut), l'algorithme est encore plus complexe et coûteux. Ensuite, les mots résultant de ce fractionnement doivent être alloués et copiés.

La partie glob sera encore plus chère. Si l' un de ces mots contiennent des caractères glob ( *, ?, [), le shell devra lire le contenu de certains répertoires et faire un peu de correspondance de motif coûteux ( bashla mise en œuvre « , par exemple , est notoirement très mal à cela).

Si l'entrée contient quelque chose comme /*/*/*/../../../*/*/*/../../../*/*/*ça, cela coûtera extrêmement cher car cela signifie répertorier des milliers d'annuaires et cela peut s'étendre à plusieurs centaines de Mio.

Ensuite, echoil effectuera généralement un traitement supplémentaire. Certaines implémentations développent des \xséquences dans l'argument qu'il reçoit, ce qui signifie analyser le contenu et probablement une autre allocation et copie des données.

D'un autre côté, OK, dans la plupart des shells catn'est pas intégré, ce qui signifie bifurquer un processus et l'exécuter (donc charger le code et les bibliothèques), mais après la première invocation, ce code et le contenu du fichier d'entrée sera mis en cache en mémoire. En revanche, il n'y aura pas d'intermédiaire. catlira de grandes quantités à la fois et l'écrira immédiatement sans traitement, et il n'a pas besoin d'allouer une énorme quantité de mémoire, juste ce tampon qu'il réutilise.

Cela signifie également qu'il est beaucoup plus fiable car il ne s'étouffe pas sur les octets NUL et ne supprime pas les caractères de fin de ligne (et ne fait pas de split + glob, bien que vous puissiez éviter cela en citant la variable et ne pas développez la séquence d'échappement bien que vous puissiez éviter cela en utilisant printfau lieu de echo).

Si vous souhaitez l'optimiser davantage, au lieu d'appeler catplusieurs fois, passez simplement inputplusieurs fois à cat.

yes input | head -n 100 | xargs cat

Exécute 3 commandes au lieu de 100.

Pour rendre la version variable plus fiable, vous devez utiliser zsh(les autres shells ne peuvent pas gérer les octets NUL) et le faire:

zmodload zsh/mapfile
var=$mapfile[input]
repeat 10 print -rn -- "$var"

Si vous savez que l'entrée ne contient pas d'octets NUL, vous pouvez le faire de manière fiable POSIXly (bien que cela puisse ne pas fonctionner là où il printfn'est pas intégré) avec:

i=$(cat input && echo .) || exit # add an extra .\n to avoid trimming newlines
i=${i%.} # remove that trailing dot (the \n was removed by cmdsubst)
n=10
while [ "$n" -gt 10 ]; do
  printf %s "$i"
  n=$((n - 1))
done

Mais cela ne sera jamais plus efficace que l'utilisation catdans la boucle (sauf si l'entrée est très petite).

Stéphane Chazelas
la source
Il convient de mentionner qu'en cas de longue dispute, vous pouvez obtenir la mémoire . Exemple/bin/echo $(perl -e 'print "A"x999999')
cuonglm
Vous vous trompez avec l'hypothèse que la taille de lecture a une influence significative, alors lisez ma réponse pour comprendre la vraie raison.
schily
@schily, faire 409600 lectures de 128 octets prend plus de temps (heure système) que 800 lectures de 64k. Comparez dd bs=128 < input > /dev/nullavec dd bs=64 < input > /dev/null. Sur les 0,6s qu'il faut à bash pour lire ce fichier, 0,4 sont dépensés dans ces readappels système dans mes tests, tandis que d'autres shells y passent beaucoup moins de temps.
Stéphane Chazelas
Eh bien, vous ne semblez pas avoir effectué une véritable analyse des performances. L'influence de l'appel de lecture (lors de la comparaison de différentes tailles de lecture) est approximative. 1% du temps total tandis que les fonctions readwc() et trim()dans le Burne Shell prennent 30% du temps total et ceci est très probablement sous-estimé car il n'y a pas de libc avec gprofannotation pour mbtowc().
schily
À qui est \xétendu?
Mohammad
11

Le problème est pas catet echo, il est sur la variable de citation oubliée $i.

Dans le script shell de type Bourne (sauf zsh), laisser des variables sans guillemets provoque des glob+splitopérateurs sur les variables.

$var

est en fait:

glob(split($var))

Ainsi, à chaque itération de boucle, tout le contenu de input(exclure les nouvelles lignes de fin) sera développé, fractionné, globlé. L'ensemble du processus nécessite un shell pour allouer de la mémoire, en analysant la chaîne encore et encore. C'est la raison pour laquelle vous avez obtenu la mauvaise performance.

Vous pouvez citer la variable à empêcher, glob+splitmais cela ne vous aidera pas beaucoup, car lorsque le shell doit encore construire le gros argument de chaîne et analyser son contenu echo(le remplacement de la fonction intégrée echopar externe /bin/echovous donnera la liste des arguments trop longue ou insuffisante en mémoire) dépendent de la $itaille). La plupart de l' echoimplémentation n'est pas compatible POSIX, elle étendra les \xséquences de barre oblique inverse dans les arguments qu'elle a reçus.

Avec cat, le shell n'a besoin que de générer un processus à chaque itération de boucle et catfera la copie d'E / S. Le système peut également mettre en cache le contenu du fichier pour accélérer le processus cat.

cuonglm
la source
2
@roaima: Vous n'avez pas mentionné la partie glob, ce qui peut être une énorme raison, imaginer quelque chose qui /*/*/*/*../../../../*/*/*/*/../../../../peut être dans le contenu du fichier. Je veux juste souligner les détails .
cuonglm
Merci Gotcha. Même sans cela, le timing double lorsque vous utilisez une variable non cotée
roaima
1
time echo $( <xdditg106) >/dev/null real 0m0.125s user 0m0.085s sys 0m0.025s time echo "$( <xdditg106)" >/dev/null real 0m0.047s user 0m0.016s sys 0m0.022s
netmonk
Je n'ai pas compris pourquoi la citation ne peut pas résoudre le problème. J'ai besoin de plus de description.
Mohammad
1
@ mohammad.k: Comme je l'ai écrit dans ma réponse, citation variable empêche la glob+splitpartie, et cela accélérera la boucle while. Et j'ai également noté que cela ne vous aiderait pas beaucoup. Depuis quand la plupart du echocomportement du shell n'est pas compatible POSIX. printf '%s' "$i"est mieux.
cuonglm
2

Si vous appelez

i=`cat input`

cela permet à votre processus shell de croître de 50 Mo jusqu'à 200 Mo (selon l'implémentation interne des caractères larges). Cela peut ralentir votre shell mais ce n'est pas le principal problème.

Le problème principal est que la commande ci-dessus doit lire l'intégralité du fichier dans la mémoire du shell et qu'il echo $ifaut effectuer une division des champs sur ce contenu de fichier dans $i. Afin de diviser les champs, tout le texte du fichier doit être converti en caractères larges et c'est là que la plupart du temps est passé.

J'ai fait quelques tests avec le cas lent et j'ai obtenu ces résultats:

  • Le plus rapide est ksh93
  • Vient ensuite mon Bourne Shell (2x plus lent que ksh93)
  • Vient ensuite bash (3x plus lent que ksh93)
  • Le dernier est ksh88 (7x plus lent que ksh93)

La raison pour laquelle ksh93 est le plus rapide semble être que ksh93 n'utilise pas mbtowc()de libc mais plutôt une implémentation propre.

BTW: Stephane se trompe que la taille de lecture a une certaine influence, j'ai compilé le Bourne Shell pour lire en blocs de 4096 octets au lieu de 128 octets et j'ai obtenu les mêmes performances dans les deux cas.

schily
la source
La i=`cat input`commande ne fait pas de fractionnement de champ, c'est le echo $icas. Le temps passé i=`cat input`sera négligeable par rapport à echo $i, mais pas par rapport à cat inputseul, et dans le cas de bash, la différence est en grande partie due à la bashréalisation de petites lectures. Passer de 128 à 4096 n'aura aucune influence sur les performances de echo $i, mais ce n'était pas le point que je faisais valoir.
Stéphane Chazelas
Notez également que les performances de echo $ivarieront considérablement en fonction du contenu de l'entrée et du système de fichiers (s'il contient des caractères IFS ou glob), c'est pourquoi je n'ai fait aucune comparaison de shells à ce sujet dans ma réponse. Par exemple, ici à la sortie de yes | ghead -c50M, ksh93 est le plus lent de tous, mais yes | ghead -c50M | paste -sd: -c'est le plus rapide.
Stéphane Chazelas
Quand je parlais du temps total, je parlais de l'implémentation entière et oui, bien sûr, le fractionnement de champ se produit avec la commande echo. et c'est là que la plupart du temps est passé.
schily
Vous avez bien sûr raison de penser que les performances dépendent du contenu de $ i.
schily
1

Dans les deux cas, la boucle ne sera exécutée que deux fois (une fois pour le mot seqet une fois pour le mot 10).

De plus, les deux fusionneront les espaces adjacents et supprimeront les espaces de début / fin, de sorte que la sortie ne soit pas nécessairement deux copies de l'entrée.

Premier

#!/bin/sh
for j in $(seq 10); do
    cat input
done >> output

Seconde

#!/bin/sh
i="$(cat input)"
for j in $(seq 10); do
    echo "$i"
done >> output

L'une des raisons pour lesquelles le echoralentissement est peut-être le fait que votre variable non citée est divisée au niveau des espaces en mots séparés. Pour 50 Mo, ce sera beaucoup de travail. Citez les variables!

Je vous suggère de corriger ces erreurs, puis de réévaluer vos horaires.


J'ai testé cela localement. J'ai créé un fichier de 50 Mo en utilisant la sortie de tar cf - | dd bs=1M count=50. J'ai également étendu les boucles pour qu'elles s'exécutent par un facteur x100 afin que les timings soient mis à l'échelle à une valeur raisonnable (j'ai ajouté une boucle supplémentaire autour de votre code entier: for k in $(seq 100); do... done). Voici les horaires:

time ./1.sh

real    0m5.948s
user    0m0.012s
sys     0m0.064s

time ./2.sh

real    0m5.639s
user    0m4.060s
sys     0m0.224s

Comme vous pouvez le voir, il n'y a pas de réelle différence, mais la version contenant echos'exécute légèrement plus rapidement. Si je supprime les guillemets et exécute votre version cassée 2, le temps double, ce qui montre que le shell doit faire beaucoup plus de travail que prévu.

time ./2original.sh

real    0m12.498s
user    0m8.645s
sys     0m2.732s
roaima
la source
En fait, la boucle s'exécute 10 fois, pas deux fois.
fpmurphy
J'ai fait ce que vous avez dit, mais le problème n'est pas résolu. catest très, très plus rapide que echo. Le premier script s'exécute en moyenne en 3 secondes, mais le second s'exécute en moyenne en 54 secondes.
Mohammad
@ fpmurphy1: Non. J'ai essayé mon code. La boucle ne s'exécute que deux fois, pas 10 fois.
Mohammad
@ mohammad.k pour la troisième fois: si vous citez vos variables, le problème disparaît.
roaima
@roaima: Que fait la commande tar cf - | dd bs=1M count=50? Fait-il un fichier régulier avec les mêmes caractères à l'intérieur? Si c'est le cas, dans mon cas, le fichier d'entrée est complètement irrégulier avec toutes sortes de caractères et d'espaces. Et encore une fois, j'ai utilisé timecomme vous l'avez utilisé, et le résultat est celui que j'ai dit: 54 secondes contre 3 secondes.
Mohammad
-1

read est beaucoup plus rapide que cat

Je pense que tout le monde peut tester cela:

$ cd /sys/devices/system/cpu/cpu0/cpufreq
───────────────────────────────────────────────────────────────────────────────────────────
$ time for ((i=0; i<10000; i++ )); do read p < scaling_cur_freq ; done

real    0m0.232s
user    0m0.139s
sys     0m0.088s
───────────────────────────────────────────────────────────────────────────────────────────
$ time for ((i=0; i<10000; i++ )); do cat scaling_cur_freq > /dev/null ; done

real    0m9.372s
user    0m7.518s
sys     0m2.435s
───────────────────────────────────────────────────────────────────────────────────────────
$ type -a read
read is a shell builtin
───────────────────────────────────────────────────────────────────────────────────────────
$ type -a cat
cat is /bin/cat

catprend 9,372 secondes. echoprend .232quelques secondes.

readest 40 fois plus rapide .

Mon premier test a $pété répété sur l'écran et a readété 48 fois plus rapide que cat.

WinEunuuchs2Unix
la source
-2

Le echoest destiné à mettre 1 ligne sur l'écran. Ce que vous faites dans le deuxième exemple, c'est que vous placez le contenu du fichier dans une variable, puis vous imprimez cette variable. Dans le premier, vous mettez immédiatement le contenu à l'écran.

catest optimisé pour cette utilisation. echon'est pas. De plus, mettre 50 Mo dans une variable d'environnement n'est pas une bonne idée.

Marco
la source
Curieuse. Pourquoi ne serait-il pas echooptimisé pour l'écriture de texte?
roaima
2
Il n'y a rien dans la norme POSIX qui dit que l'écho est censé mettre une ligne sur un écran.
fpmurphy
-2

Il ne s'agit pas d'écho plus rapide, mais de ce que vous faites:

Dans un cas, vous lisez directement depuis l'entrée et écrivez vers la sortie. En d'autres termes, tout ce qui est lu depuis l'entrée via cat, va vers la sortie via stdout.

input -> output

Dans l'autre cas, vous lisez depuis l'entrée dans une variable en mémoire, puis vous écrivez le contenu de la variable en sortie.

input -> variable
variable -> output

Ce dernier sera beaucoup plus lent, surtout si l'entrée est de 50 Mo.

Aleksander
la source
Je pense que vous devez mentionner que cat doit ouvrir le fichier en plus de copier depuis stdin et de l'écrire sur stdout. C'est l'excellence du deuxième script, mais le premier est bien meilleur que le second au total.
Mohammad
Il n'y a pas d'excellence dans le deuxième script; cat doit ouvrir le fichier d'entrée dans les deux cas. Dans le premier cas, la sortie standard de cat va directement au fichier. Dans le second cas, la sortie standard de cat va d'abord à une variable, puis vous imprimez la variable dans le fichier de sortie.
Aleksander
@ mohammad.k, il n'y a catégoriquement pas "excellence" dans le second script.
Wildcard