Bash ma problemy z wydajnością przy użyciu list argumentów?

11

Rozwiązany w bash 5.0

tło

Dla tła (i zrozumienia (i starania się unikać głosów negatywnych to pytanie wydaje się przyciągać)) wyjaśnię ścieżkę, która doprowadziła mnie do tego problemu (najlepiej, co mogę sobie przypomnieć dwa miesiące później).

Załóżmy, że wykonujesz kilka testów powłoki dla listy znaków Unicode:

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

a istnieje ponad 1 milion znaków Unicode, testowanie 20 000 z nich nie wydaje się aż tak duże.
Załóżmy również, że ustawiłeś znaki jako argumenty pozycyjne:

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

z zamiarem przekazania znaków do każdej funkcji i przetworzenia ich na różne sposoby. Więc funkcje powinny mieć formę test1 "$@"lub podobny. Teraz zdaję sobie sprawę, jak zły jest ten pomysł.

Załóżmy teraz, że potrzeba czasu (n = 1000) na każde rozwiązanie, aby dowiedzieć się, co jest lepsze, w takich warunkach zakończy się struktura podobna do:

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

Funkcje test#są bardzo proste, aby je tutaj zaprezentować.
Oryginały były stopniowo przycinane, aby dowiedzieć się, gdzie było ogromne opóźnienie.

Powyższy skrypt działa, możesz go uruchomić i tracić kilka sekund, robiąc bardzo mało.

W trakcie procesu upraszczania, aby znaleźć dokładnie, gdzie było opóźnienie (a redukcja każdej funkcji testowej do niemal zera jest ekstremalna po wielu próbach), postanowiłem usunąć przekazywanie argumentów do każdej funkcji testowej, aby dowiedzieć się, jak bardzo poprawił się czas, tylko współczynnik 6, niewiele.

Aby spróbować, usuń wszystkie "$@"funkcje wejścia main1(lub wykonaj kopię) i przetestuj ponownie (lub oba main1i kopię main2(z main2 "$@")), aby porównać. Jest to podstawowa struktura poniżej w oryginalnym poście (OP).

Ale zastanawiałem się: dlaczego powłoka tak długo „nic nie robi”? Tak, tylko „kilka sekund”, ale wciąż dlaczego?

To sprawiło, że przetestowałem w innych powłokach, aby odkryć, że tylko bash miał ten problem.
Spróbuj ksh ./script(taki sam skrypt jak powyżej).

Doprowadziło to do tego opisu: wywołanie funkcji ( test#) bez żadnego argumentu zostaje opóźnione przez argumenty w obiekcie parent ( main#). To jest poniższy opis i był to oryginalny post (OP) poniżej.

Oryginalny post.

Wywołanie funkcji (w Bash 4.4.12 (1) -release) nic nie robić f1(){ :; }jest tysiąc razy wolniej niż :ale tylko jeśli istnieją argumenty zdefiniowane w dominującej funkcji wywołującej, dlaczego?

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

Wyniki 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

Nie ma żadnych argumentów ani danych wejściowych lub wyjściowych używanych w funkcji f1, opóźnienie rzędu tysiąca (1000) jest nieoczekiwane. 1


Rozszerzając testy na kilka powłok, wyniki są spójne, większość powłok nie ma problemów ani opóźnień (używane są takie same wartości im):

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

Wyniki:

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

Odkomentuj pozostałe dwa testy, aby potwierdzić, że ani seqprzetwarzanie listy argumentów nie jest źródłem opóźnienia.

1 Towiadomo, że przekazując wyniki argumenty zwiększy czas wykonania. Dzięki@slm

Izaak
źródło
3
Zapisane przez efekt meta. unix.meta.stackexchange.com/q/5021/3562
Joshua

Odpowiedzi:

9

Skopiowano z: Dlaczego opóźnienie w pętli? na twoją prośbę:

Możesz skrócić przypadek testowy do:

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

Wywołuje funkcję, gdy $@jest duża, co wydaje się ją uruchamiać.

Domyślam się, że czas poświęca się $@na oszczędzanie na stosie i późniejsze jego przywracanie. Być może bashrobi to bardzo nieefektywnie, powielając wszystkie wartości lub coś w tym rodzaju. Czas wydaje się być o (n²).

Ten sam czas masz w innych powłokach na:

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

To tutaj przekazujesz listę argumentów do funkcji i tym razem powłoka musi skopiować wartości ( bashkończy się to dla tego 5 razy wolniej).

(Początkowo myślałem, że jest gorzej w bash 5 (obecnie w wersji alfa), ale było to spowodowane debugowaniem malloc w wersjach programistycznych, jak zauważył @egmont; sprawdź także, jak buduje się twoja dystrybucja, bashjeśli chcesz porównać własną kompilację z system jeden. Na przykład Ubuntu używa --without-bash-malloc)

Stéphane Chazelas
źródło
Jak usuwa się debugowanie?
Izaak,
@isaac, zrobiłem to, zmieniając RELSTATUS=alphana RELSTATUS=releasew configureskrypcie.
Stéphane Chazelas,
Dodano wyniki testu dla obu --without-bash-malloci RELSTATUS=releasedo wyników pytań. To wciąż pokazuje problem z połączeniem do f.
Izaak,
@Isaac, tak, właśnie powiedziałem, że myliłem się, mówiąc, że w bash5 było gorzej. Nie jest gorzej, jest tak samo źle.
Stéphane Chazelas,
Nie, nie jest tak źle . Bash5 rozwiązuje problem z dzwonieniem :i nieco poprawia dzwonienie f. Spójrz na czasy testu2 w pytaniu.
Izaak,