Bash a des problèmes de performances en utilisant des listes d'arguments?

11

Résolu dans bash 5.0

Contexte

Pour le contexte (et la compréhension (et essayer d'éviter les votes négatifs que cette question semble attirer)), je vais expliquer le chemin qui m'a amené à ce problème (enfin, le meilleur dont je me souvienne deux mois plus tard).

Supposons que vous effectuez des tests shell pour une liste de caractères Unicode:

printf "$(printf '\\U%x ' {33..200})"

et comme il y a plus d'un million de caractères Unicode, tester 20 000 d'entre eux ne semble pas être beaucoup.
Supposons également que vous définissez les caractères comme arguments de position:

set -- $(printf "$(printf '\\U%x ' {33..20000})")

avec l'intention de passer les caractères à chaque fonction pour les traiter de différentes manières. Les fonctions doivent donc avoir la forme test1 "$@"ou similaire. Maintenant, je me rends compte à quel point cette idée est mauvaise dans bash.

Supposons maintenant qu'il soit nécessaire de chronométrer (an n = 1000) chaque solution pour savoir laquelle est la meilleure, dans de telles conditions, vous vous retrouverez avec une structure similaire à:

#!/bin/bash --
TIMEFORMAT='real: %R'  # '%R %U %S'

set -- $(printf "$(printf '\\U%x ' {33..20000})")
n=1000

test1(){ echo "$1"; } >/dev/null
test2(){ echo "$#"; } >/dev/null
test3(){ :; }

main1(){ time for i in $(seq $n); do test1 "$@"; done
         time for i in $(seq $n); do test2 "$@"; done
         time for i in $(seq $n); do test3 "$@"; done
       }

main1 "$@"

Les fonctions test#sont rendues très très simples juste pour être présentées ici.
Les originaux ont été progressivement coupés pour trouver où se trouvait l'énorme retard.

Le script ci-dessus fonctionne, vous pouvez l'exécuter et perdre quelques secondes à faire très peu.

Dans le processus de simplification pour trouver exactement où était le retard (et réduire chaque fonction de test à presque rien n'est extrême après de nombreux essais), j'ai décidé de supprimer le passage d'arguments à chaque fonction de test pour savoir combien le temps s'était amélioré, seulement un facteur de 6, pas beaucoup.

Pour vous essayer, supprimez toutes les "$@"fonctions in main1(ou faites une copie) et testez à nouveau (ou les deux main1et la copie main2(avec main2 "$@")) pour comparer. Il s'agit de la structure de base ci-dessous dans le message d'origine (OP).

Mais je me suis demandé: pourquoi la coque met-elle autant de temps à "ne rien faire"?. Oui, seulement "quelques secondes", mais pourquoi?.

Cela m'a fait tester dans d'autres shells pour découvrir que seul bash avait ce problème.
Essayez ksh ./script(le même script que ci-dessus).

Cela conduit à cette description: l'appel d'une fonction ( test#) sans aucun argument est retardé par les arguments du parent ( main#). C'est la description qui suit et c'était le post original (OP) ci-dessous.

Poste d'origine.

Appeler une fonction (dans Bash 4.4.12 (1) -release) pour ne rien faire f1(){ :; }est mille fois plus lent que :mais uniquement si des arguments sont définis dans la fonction d'appel parent , pourquoi?

#!/bin/bash
TIMEFORMAT='real: %R'

f1   () { :; }

f2   () {
   echo "                     args = $#";
   printf '1 function no   args yes '; time for ((i=1;i<$n;i++)); do  :   ; done 
   printf '2 function yes  args yes '; time for ((i=1;i<$n;i++)); do  f1  ; done
   set --
   printf '3 function yes  args no  '; time for ((i=1;i<$n;i++)); do  f1  ; done
   echo
        }

main1() { set -- $(seq $m)
          f2  ""
          f2 "$@"
        }

n=1000; m=20000; main1

Résultats de test1:

                     args = 1
1 function no   args yes real:  0.013
2 function yes  args yes real:  0.024
3 function yes  args no  real:  0.020

                     args = 20000
1 function no   args yes real:  0.010
2 function yes  args yes real: 20.326
3 function yes  args no  real:  0.019

Il n'y a pas d'arguments ni d'entrée ou de sortie utilisés dans la fonction f1, le retard d'un facteur de mille (1000) est inattendu. 1


En étendant les tests à plusieurs coques, les résultats sont cohérents, la plupart des coques n'ont aucun problème ni souffrent de retards (les mêmes n et m sont utilisés):

test2(){
          for sh in dash mksh ksh zsh bash b50sh
      do
          echo "$sh" >&2
#         \time -f '\t%E' seq "$m" >/dev/null
#         \time -f '\t%E' "$sh" -c 'set -- $(seq '"$m"'); for i do :; done'
          \time -f '\t%E' "$sh" -c 'f(){ :;}; while [ "$((i+=1))" -lt '"$n"' ]; do : ; done;' $(seq $m)
          \time -f '\t%E' "$sh" -c 'f(){ :;}; while [ "$((i+=1))" -lt '"$n"' ]; do f ; done;' $(seq $m)
      done
}

test2

Résultats:

dash
        0:00.01
        0:00.01
mksh
        0:00.01
        0:00.02
ksh
        0:00.01
        0:00.02
zsh
        0:00.02
        0:00.04
bash
        0:10.71
        0:30.03
b55sh             # --without-bash-malloc
        0:00.04
        0:17.11
b56sh             # RELSTATUS=release
        0:00.03
        0:15.47
b50sh             # Debug enabled (RELSTATUS=alpha)
        0:04.62
        xxxxxxx    More than a day ......

Décommentez les deux autres tests pour confirmer qu'aucun des deux seqou le traitement de la liste d'arguments n'est à l'origine du retard.

1 Ilest connu que le passage des résultats par des arguments augmentera le temps d'exécution. Merci@slm

Isaac
la source
3
Enregistré par l'effet méta. unix.meta.stackexchange.com/q/5021/3562
Joshua

Réponses:

9

Copié de: Pourquoi le retard dans la boucle? à votre demande:

Vous pouvez raccourcir le scénario de test pour:

time bash -c 'f(){ :;};for i do f; done' {0..10000}

C'est appeler une fonction tant qu'elle $@est grande qui semble la déclencher.

Je suppose que le temps est consacré à l'enregistrement $@sur une pile et à sa restauration par la suite. Peut bash- être le fait très inefficacement en dupliquant toutes les valeurs ou quelque chose comme ça. Le temps semble être en o (n²).

Vous obtenez le même genre de temps dans d'autres coquilles pour:

time zsh -c 'f(){ :;};for i do f "$@"; done' {0..10000}

C'est là que vous passez la liste des arguments aux fonctions, et cette fois, le shell doit copier les valeurs ( bashfinit par être 5 fois plus lent pour celle-ci).

(Au départ, je pensais que c'était pire dans bash 5 (actuellement en alpha), mais cela était dû au fait que le débogage malloc était activé dans les versions de développement comme indiqué par @egmont; vérifiez également comment votre distribution se construit bashsi vous souhaitez comparer votre propre construction avec le système. Par exemple, Ubuntu utilise --without-bash-malloc)

Stéphane Chazelas
la source
Comment le débogage est-il supprimé?
Isaac
@isaac, je l'ai fait en passant RELSTATUS=alphaà RELSTATUS=releasedans le configurescript.
Stéphane Chazelas
Ajout de résultats de test pour les deux --without-bash-mallocet RELSTATUS=releaseaux résultats de la question. Cela montre toujours un problème avec l'appel à f.
Isaac
@Isaac, oui, je viens de dire que j'avais tort de dire que c'était pire en bash5. Ce n'est pas pire, c'est tout aussi mauvais.
Stéphane Chazelas
Non, ce n'est pas aussi mauvais . Bash5 résout le problème de l'appel :et améliore un peu l'appel f. Regardez les horaires de test2 dans la question.
Isaac