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 cat
qui 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 echo
et cat
pour 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,echo
il 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 commei
. Cependant, ce n'est qu'une supposition.) - Et pourquoi j'ai des problèmes lorsque j'utilise
echo
?
MISE À JOUR
J'ai utilisé seq 10
au 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: cat
fonctionne beaucoup plus rapidement que echo
.
cat $(for i in $(seq 1 10); do echo "input"; done) >> output
? :)echo
plus rapide. Ce qui vous manque, c'est que vous faites trop travailler le shell en ne citant pas les variables lorsque vous les utilisez.printf '%s' "$i"
, nonecho $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/…Réponses:
Il y a plusieurs choses à considérer ici.
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
i
variable 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.zsh
doit effectuer un post-traitement pour échapper aux octets NUL (d'autres shells se brisent sur les octets NUL), etyash
effectue 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,
echo
est 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
yash
fait le cas) , recherchez les$IFS
caractères dans la chaîne. S'il$IFS
contient 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 (bash
la 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,
echo
il effectuera généralement un traitement supplémentaire. Certaines implémentations développent des\x
sé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
cat
n'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.cat
lira 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
printf
au lieu deecho
).Si vous souhaitez l'optimiser davantage, au lieu d'appeler
cat
plusieurs fois, passez simplementinput
plusieurs fois à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: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
printf
n'est pas intégré) avec:Mais cela ne sera jamais plus efficace que l'utilisation
cat
dans la boucle (sauf si l'entrée est très petite).la source
/bin/echo $(perl -e 'print "A"x999999')
dd bs=128 < input > /dev/null
avecdd bs=64 < input > /dev/null
. Sur les 0,6s qu'il faut à bash pour lire ce fichier, 0,4 sont dépensés dans cesread
appels système dans mes tests, tandis que d'autres shells y passent beaucoup moins de temps.readwc()
ettrim()
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 avecgprof
annotation pourmbtowc()
.\x
étendu?Le problème est pas
cat
etecho
, 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 desglob+split
opérateurs sur les variables.est en fait:
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+split
mais cela ne vous aidera pas beaucoup, car lorsque le shell doit encore construire le gros argument de chaîne et analyser son contenuecho
(le remplacement de la fonction intégréeecho
par externe/bin/echo
vous donnera la liste des arguments trop longue ou insuffisante en mémoire) dépendent de la$i
taille). La plupart de l'echo
implémentation n'est pas compatible POSIX, elle étendra les\x
sé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 etcat
fera la copie d'E / S. Le système peut également mettre en cache le contenu du fichier pour accélérer le processus cat.la source
/*/*/*/*../../../../*/*/*/*/../../../../
peut être dans le contenu du fichier. Je veux juste souligner les détails .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
glob+split
partie, et cela accélérera la boucle while. Et j'ai également noté que cela ne vous aiderait pas beaucoup. Depuis quand la plupart duecho
comportement du shell n'est pas compatible POSIX.printf '%s' "$i"
est mieux.Si vous appelez
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 $i
faut 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:
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.
la source
i=`cat input`
commande ne fait pas de fractionnement de champ, c'est leecho $i
cas. Le temps passéi=`cat input`
sera négligeable par rapport àecho $i
, mais pas par rapport àcat input
seul, et dans le cas debash
, la différence est en grande partie due à labash
réalisation de petites lectures. Passer de 128 à 4096 n'aura aucune influence sur les performances deecho $i
, mais ce n'était pas le point que je faisais valoir.echo $i
varieront 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 deyes | ghead -c50M
, ksh93 est le plus lent de tous, maisyes | ghead -c50M | paste -sd: -
c'est le plus rapide.Dans les deux cas, la boucle ne sera exécutée que deux fois (une fois pour le mot
seq
et une fois pour le mot10
).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
Seconde
L'une des raisons pour lesquelles le
echo
ralentissement 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:Comme vous pouvez le voir, il n'y a pas de réelle différence, mais la version contenant
echo
s'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.la source
cat
est très, très plus rapide queecho
. Le premier script s'exécute en moyenne en 3 secondes, mais le second s'exécute en moyenne en 54 secondes.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étime
comme vous l'avez utilisé, et le résultat est celui que j'ai dit: 54 secondes contre 3 secondes.read
est beaucoup plus rapide quecat
Je pense que tout le monde peut tester cela:
cat
prend 9,372 secondes.echo
prend.232
quelques secondes.read
est 40 fois plus rapide .Mon premier test a
$p
été répété sur l'écran et aread
été 48 fois plus rapide quecat
.la source
Le
echo
est 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.cat
est optimisé pour cette utilisation.echo
n'est pas. De plus, mettre 50 Mo dans une variable d'environnement n'est pas une bonne idée.la source
echo
optimisé pour l'écriture de texte?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.
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.
Ce dernier sera beaucoup plus lent, surtout si l'entrée est de 50 Mo.
la source