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 main1
i 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 seq
przetwarzanie listy argumentów nie jest źródłem opóźnienia.
1 Towiadomo, że przekazując wyniki argumenty zwiększy czas wykonania. Dzięki@slm
Odpowiedzi:
Skopiowano z: Dlaczego opóźnienie w pętli? na twoją prośbę:
Możesz skrócić przypadek testowy do:
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żebash
robi 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:
To tutaj przekazujesz listę argumentów do funkcji i tym razem powłoka musi skopiować wartości (
bash
koń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,
bash
jeśli chcesz porównać własną kompilację z system jeden. Na przykład Ubuntu używa--without-bash-malloc
)źródło
RELSTATUS=alpha
naRELSTATUS=release
wconfigure
skrypcie.--without-bash-malloc
iRELSTATUS=release
do wyników pytań. To wciąż pokazuje problem z połączeniem do f.:
i nieco poprawia dzwonienief
. Spójrz na czasy testu2 w pytaniu.