Bash hat Leistungsprobleme bei der Verwendung von Argumentlisten?

11

In Bash 5.0 gelöst

Hintergrund

Zum Hintergrund (und zum Verständnis (und zum Versuch, die Abstimmungen zu vermeiden, die diese Frage anzieht) scheint ich den Weg zu erklären, der mich zu diesem Thema geführt hat (das Beste, an das ich mich zwei Monate später erinnern kann).

Angenommen, Sie führen einige Shell-Tests für eine Liste von Unicode-Zeichen durch:

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

und da es mehr als 1 Million Unicode-Zeichen gibt, scheint es nicht so viel zu sein, 20.000 davon zu testen.
Nehmen Sie außerdem an, dass Sie die Zeichen als Positionsargumente festlegen:

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

mit der Absicht, die Zeichen an jede Funktion zu übergeben, um sie auf unterschiedliche Weise zu verarbeiten. Die Funktionen sollten also die Form test1 "$@"oder ähnliches haben. Jetzt merke ich, wie schlecht die Idee ist.

Nehmen wir nun an, dass jede Lösung zeitlich festgelegt werden muss (n = 1000), um herauszufinden, welche Lösung besser ist. Unter solchen Bedingungen erhalten Sie eine Struktur ähnlich der folgenden:

#!/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 "$@"

Die Funktionen test#sind sehr, sehr einfach gemacht, nur um hier vorgestellt zu werden.
Die Originale wurden nach und nach gekürzt, um herauszufinden, wo die große Verzögerung lag.

Das obige Skript funktioniert, Sie können es ausführen und einige Sekunden damit verschwenden, sehr wenig zu tun.

In dem Prozess der Vereinfachung, um genau zu finden, wo die Verzögerung war (und das Reduzieren jeder Testfunktion auf fast nichts ist das Extrem nach vielen Versuchen), habe ich beschlossen, die Übergabe von Argumenten an jede Testfunktion zu entfernen, um herauszufinden, um wie viel sich die Zeit verbessert hat ein Faktor von 6, nicht viel.

Um es selbst zu versuchen, entfernen Sie alle "$@"In-Funktionen main1(oder erstellen Sie eine Kopie) und testen Sie sie erneut (oder beide main1und die Kopie main2(mit main2 "$@")), um sie zu vergleichen. Dies ist die Grundstruktur unten im ursprünglichen Beitrag (OP).

Aber ich fragte mich: Warum braucht die Shell so lange, um "nichts zu tun"? Ja, nur "ein paar Sekunden", aber trotzdem, warum?

Dies ließ mich in anderen Shells testen, um festzustellen, dass nur Bash dieses Problem hatte.
Versuchen Sie es ksh ./script(das gleiche Skript wie oben).

Dies führte zu dieser Beschreibung: Der Aufruf einer function ( test#) ohne Argument wird durch die Argumente in parent ( main#) verzögert . Dies ist die folgende Beschreibung und war der ursprüngliche Beitrag (OP) unten.

Ursprünglicher Beitrag.

Das Aufrufen einer Funktion (in Bash 4.4.12 (1) -release), um nichts zu tun, f1(){ :; }ist tausendmal langsamer als, :aber nur, wenn in der übergeordneten Aufruffunktion Argumente definiert sind. Warum?

#!/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

Ergebnisse von 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

In der Funktion werden weder Argumente noch Eingaben oder Ausgaben verwendet f1. Die Verzögerung um den Faktor tausend (1000) ist unerwartet. 1


Wenn die Tests auf mehrere Schalen ausgedehnt werden, sind die Ergebnisse konsistent. Die meisten Schalen haben keine Probleme und leiden nicht unter Verzögerungen (es werden die gleichen n und m verwendet):

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

Ergebnisse:

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 ......

Kommentieren Sie die beiden anderen Tests aus, um zu bestätigen, dass weder die seqVerarbeitung noch die Verarbeitung der Argumentliste die Ursache für die Verzögerung sind.

1 Esist bekannt, dass das Übergeben von Ergebnissen durch Argumente die Ausführungszeit verlängert. Danke@slm

Isaac
quelle
3
Durch den Metaeffekt gespeichert. unix.meta.stackexchange.com/q/5021/3562
Joshua

Antworten:

9

Kopiert von: Warum die Verzögerung in der Schleife? auf deine Anfrage:

Sie können den Testfall verkürzen auf:

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

Es ruft eine Funktion auf, während sie $@groß ist, die sie auszulösen scheint.

Ich würde vermuten, dass die Zeit damit verbracht wird, $@auf einem Stapel zu sparen und ihn anschließend wiederherzustellen. Möglicherweise bashfunktioniert es sehr ineffizient, indem alle Werte oder ähnliches dupliziert werden. Die Zeit scheint in o (n²) zu sein.

Sie erhalten die gleiche Zeit in anderen Muscheln für:

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

Das ist , wo Sie die Liste der Argumente an die Funktionen übergeben tun, und dieses Mal die Schale muss die Werte (zum Kopieren bashendet als 5 - mal so langsam für diesen einen).

(Ich dachte anfangs, dass es in Bash 5 (derzeit in Alpha) schlimmer war, aber das lag daran, dass das Malloc-Debugging in Entwicklungsversionen aktiviert wurde, wie von @egmont angegeben. Überprüfen Sie auch, wie Ihre Distribution erstellt wird, bashwenn Sie Ihren eigenen Build mit dem vergleichen möchten Zum Beispiel verwendet Ubuntu --without-bash-malloc)

Stéphane Chazelas
quelle
Wie wird das Debuggen entfernt?
Isaac
@isaac, ich habe es getan, indem ich zu gewechselt RELSTATUS=alphahabeRELSTATUS=release im configureSkript.
Stéphane Chazelas
Testergebnisse für --without-bash-mallocund hinzugefügtRELSTATUS=release zu den Frageergebnissen . Das zeigt immer noch ein Problem mit dem Aufruf von f.
Isaac
@Isaac, ja, ich habe nur gesagt, dass ich mich geirrt habe zu sagen, dass es in bash5 schlimmer war. Es ist nicht schlimmer, es ist genauso schlimm.
Stéphane Chazelas
Nein ist es nicht so schlimm . Bash5 löst das Problem beim Anrufen :und verbessert das Anrufen ein wenig f. Schauen Sie sich die Test2-Timings in der Frage an.
Isaac