Czy coś jest nie tak z moim skryptem, czy Bash jest znacznie wolniejszy niż Python?


29

Testowałem szybkość Basha i Pythona, uruchamiając pętlę 1 miliard razy.

$ cat python.py
#!/bin/python
# python v3.5
i=0;
while i<=1000000000:
    i=i+1;

Kod Bash:

$ cat bash2.sh
#!/bin/bash
# bash v4.3
i=0
while [[ $i -le 1000000000 ]]
do
let i++
done

Za pomocą timepolecenia dowiedziałem się, że kod Pythona zajmuje tylko 48 sekund, a kod Bash zajął ponad godzinę, zanim zabiłem skrypt.

Dlaczego tak jest? Spodziewałem się, że Bash będzie szybszy. Czy coś jest nie tak z moim skryptem, czy Bash jest naprawdę wolniejszy z tym skryptem?


49
Nie jestem do końca pewien, dlaczego spodziewałeś się, że Bash będzie szybszy niż Python.
Kusalananda

9
@MatijaNalis nie, nie możesz! Skrypt jest ładowany do pamięci, edycja pliku tekstowego, z którego został odczytany (plik skryptu), nie będzie miała absolutnie żadnego wpływu na uruchomiony skrypt. Dobrą rzeczą jest to, że bash jest już wystarczająco wolny, bez konieczności otwierania i ponownego odczytu pliku przy każdym uruchomieniu pętli!
terdon


4
Bash odczytuje plik linia po linii podczas wykonywania, ale pamięta, co przeczytał, jeśli dojdzie do tej linii ponownie (ponieważ jest w pętli lub funkcji). Pierwotne twierdzenie o ponownym czytaniu każdej iteracji nie jest prawdziwe, ale zmiany linii, które mają być osiągnięte, będą skuteczne. Ciekawa demonstracja: stwórz plik zawierający echo echo hello >> $0i uruchom go.
Michael Homer,

3
@MatijaNalis ah, OK, rozumiem to. Rzucił mnie pomysł zmiany pętli biegowej. Przypuszczalnie każda linia jest odczytywana sekwencyjnie i dopiero po zakończeniu ostatniej. Pętla jest jednak traktowana jako pojedyncze polecenie i będzie czytana w całości, więc jej zmiana nie wpłynie na uruchomiony proces. Co ciekawe, zawsze zakładałem, że cały skrypt jest ładowany do pamięci przed wykonaniem. Dzięki za wskazanie tego!
terdon

Odpowiedzi:


17

Jest to znany błąd w bash; zobacz stronę podręcznika i wyszukaj „BŁĘDY”:

BUGS
       It's too big and too slow.

;)


Aby uzyskać doskonałe informacje na temat różnic koncepcyjnych między skryptami powłoki a innymi językami programowania, bardzo polecam przeczytanie:

Najważniejsze fragmenty:

Muszle są językiem wyższego poziomu. Można powiedzieć, że to nawet nie język. Są przed wszystkimi interpretatorami wiersza poleceń. Zadanie jest wykonywane przez te polecenia, które uruchamiasz, a powłoka służy wyłącznie do ich uporządkowania.

...

IOW, w powłokach, szczególnie do przetwarzania tekstu, wywołujesz jak najmniej narzędzi i pozwalasz im współpracować z zadaniem, a nie uruchamiasz tysiące narzędzi w kolejności, czekając na uruchomienie, uruchomienie i oczyszczenie każdego z nich przed uruchomieniem następnego.

...

Jak wspomniano wcześniej, uruchomienie jednego polecenia ma swój koszt. Ogromny koszt, jeśli to polecenie nie jest wbudowane, ale nawet jeśli są wbudowane, koszt jest duży.

Powłoki nie zostały zaprojektowane do takiego działania, nie mają pretensji do bycia wydajnymi językami programowania. Nie są, są tylko interpretatorami wiersza poleceń. Tak więc niewiele zostało zoptymalizowanych na tym froncie.


Nie używaj dużych pętli w skryptach powłoki.


54

Pętle powłoki są powolne, a bash są najwolniejsze. Pociski nie są przeznaczone do wykonywania ciężkiej pracy w pętlach. Powłoki mają na celu uruchomienie kilku zewnętrznych, zoptymalizowanych procesów na partiach danych.


W każdym razie byłem ciekawy, jak porównują się pętle powłoki, więc zrobiłem mały test porównawczy:

#!/bin/bash

export IT=$((10**6))

echo POSIX:
for sh in dash bash ksh zsh; do
    TIMEFORMAT="%RR %UU %SS $sh"
    time $sh -c 'i=0; while [ "$IT" -gt "$i" ]; do i=$((i+1)); done'
done


echo C-LIKE:
for sh in bash ksh zsh; do
    TIMEFORMAT="%RR %UU %SS $sh"
    time $sh -c 'for ((i=0;i<IT;i++)); do :; done'
done

G=$((10**9))
TIMEFORMAT="%RR %UU %SS 1000*C"
echo 'int main(){ int i,sum; for(i=0;i<IT;i++) sum+=i; printf("%d\n", sum); return 0; }' |
   gcc -include stdio.h -O3 -x c -DIT=$G - 
time ./a.out

( Szczegóły:

  • Procesor: Intel (R) Core (TM) i5 CPU M 430 @ 2.27GHz
  • ksh: wersja sh (AT&T Research) 93u + 01.08.2012
  • bash: GNU bash, wersja 4.3.11 (1) -release (x86_64-pc-linux-gnu)
  • zsh: zsh 5.2 (x86_64-unknown-linux-gnu)
  • myślnik: 0,5,7-4ubuntu1

)

(Skrócone) wyniki (czas na iterację) to:

POSIX:
5.8 µs  dash
8.5 µs ksh
14.6 µs zsh
22.6 µs bash

C-LIKE:
2.7 µs ksh
5.8 µs zsh
11.7 µs bash

C:
0.4 ns C

Z wyników:

Jeśli chcesz nieco szybszej pętli powłoki, to jeśli masz [[składnię i potrzebujesz szybkiej pętli powłoki, jesteś w zaawansowanej powłoce i masz również C-for for loop. Następnie użyj pętli C jak dla. Mogą być około 2 razy szybsze niż while [pętle w tej samej skorupie.

  • ksh ma najszybszą for (pętlę około 2,7 µs na iterację
  • dash ma najszybszą while [pętlę około 5,8 µs na iterację

C dla pętli może być o 3-4 rzędy dziesiętne wielkości szybsze. (Słyszałem, że Torvaldowie kochają C).

Zoptymalizowana pętla C dla pętli jest 56500 razy szybsza niż while [pętla bash (najwolniejsza pętla powłoki) i 6750 razy szybsza niż pętla kshfor ( pętla (najszybsza pętla powłoki).


Ponownie powolność powłok nie powinna mieć większego znaczenia, ponieważ typowym wzorem dla powłok jest odciążenie kilku procesów zewnętrznych, zoptymalizowanych programów.

Przy takim wzorcu powłoki często znacznie ułatwiają pisanie skryptów o wydajności przewyższającej skrypty Pythona (ostatnim razem, gdy sprawdzałem, tworzenie potoków procesu w Pythonie było dość niezdarne).

Kolejną rzeczą do rozważenia jest czas uruchamiania.

time python3 -c ' '

trwa 30 do 40 ms na moim komputerze, podczas gdy pociski trwają około 3 ms. Jeśli uruchamiasz wiele skryptów, szybko się to sumuje i możesz zrobić bardzo dużo w dodatkowych 27-37 ms, których uruchomienie zajmuje Python. Małe skrypty można ukończyć kilka razy w tym czasie.

(NodeJs jest prawdopodobnie najgorszym środowiskiem uruchomieniowym skryptów w tym dziale, ponieważ jego uruchomienie zajmuje około 100 ms (chociaż po jego uruchomieniu trudno byłoby znaleźć lepszą wydajność wśród języków skryptowych)).


Dla ksh, może chcesz określić realizację (AT & T ksh88, AT & T ksh93, pdksh, mksh...) jak jest sporo różnic między nimi. Na bash, można określić wersję. Ostatnio poczynił pewne postępy (dotyczy to również innych powłok).
Stéphane Chazelas,

@ StéphaneChazelas Thanks. Dodałem wersje używanego oprogramowania i sprzętu.
PSkocik,

Dla porównania: stworzenie rurociągu procesowego w Pythonie musisz zrobić coś takiego: from subprocess import *; p1=Popen(['echo', 'something'], stdout=PIPE); p2 = Popen(['grep', 'pattern'], stdin=p1.stdout, stdout=PIPE); Popen(['wc', '-c'], stdin=PIPE). Jest to rzeczywiście niezdarne, ale kodowanie pipelinefunkcji, która robi to za Ciebie dla dowolnej liczby procesów, nie powinno być trudne pipeline(['echo', 'something'], ['grep', 'patter'], ['wc', '-c']).
Bakuriu,

1
Pomyślałem, że może optymalizator gcc całkowicie eliminuje pętlę. Tak nie jest, ale nadal robi interesującą optymalizację: używa instrukcji SIMD, aby wykonać 4 dodania równolegle, zmniejszając liczbę iteracji pętli do 250000.
Mark Plotnick

1
@PSkocik: Na granicy tego, co optymalizatorzy mogą zrobić w 2016 roku. Wygląda na to, że C ++ 17 wymaga, aby kompilatory były w stanie obliczyć podobne wyrażenia w czasie kompilacji (nawet jako optymalizacja). Dzięki tej możliwości C ++ GCC może uznać ją za optymalizację również dla języka C.
MSalters

18

Zrobiłem trochę testów i na moim systemie uruchomiłem następujące - żadne nie przyspieszyło rzędu wielkości, które byłyby potrzebne, aby być konkurencyjnym, ale możesz zrobić to szybciej:

Test 1: 18,233 s

#!/bin/bash
i=0
while [[ $i -le 4000000 ]]
do
    let i++
done

test2: 20,45s

#!/bin/bash
i=0
while [[ $i -le 4000000 ]]
do 
    i=$(($i+1))
done

test3: 17,64s

#!/bin/bash
i=0
while [[ $i -le 4000000 ]]; do let i++; done

test4: 26,69s

#!/bin/bash
i=0
while [ $i -le 4000000 ]; do let i++; done

test5: 12,79s

#!/bin/bash
export LC_ALL=C

for ((i=0; i != 4000000; i++)) { 
:
}

Ważną częścią tego ostatniego jest eksport LC_ALL = C. Przekonałem się, że wiele operacji bash kończy się znacznie szybciej, jeśli jest używana, w szczególności jakakolwiek funkcja wyrażenia regularnego. Pokazuje także nieudokumentowaną składnię do używania {} i: jako no-op.


3
+1 za sugestię LC_ALL, nie wiedziałem o tym.
einpoklum

+1 Ciekawe, jak to [[jest o wiele szybsze niż [. Nie wiedziałem, że LC_ALL = C (BTW nie trzeba go eksportować) zrobiło różnicę.
PSkocik,

@PSkocik O ile mi wiadomo, [[jest wbudowaną wersją bash i [jest tak naprawdę /bin/[, podobnie jak /bin/test- programem zewnętrznym. I dlatego są wolniejsze.
tomsmeding

@tomsmending [jest wbudowany we wszystkie popularne powłoki (try type [). Program zewnętrzny jest obecnie w większości nieużywany.
PSkocik,

10

Powłoka jest wydajna, jeśli używasz jej do tego, do czego została zaprojektowana (chociaż wydajność rzadko jest tym, czego szukasz w powłoce).

Powłoka to interpreter wiersza poleceń, przeznaczony jest do uruchamiania poleceń i współpracy ich z zadaniem.

Jeśli chcesz liczyć na 1000000000, należy powołać się na (jeden) polecenie, aby policzyć, jak seq, bc, awklub python/ perl... 1000000000 Uruchamianie [[...]]poleceń i 1000000000 letpolecenia jest zobowiązany być strasznie nieskuteczny, zwłaszcza z bashktórych powłoka jest najwolniejsza ze wszystkich.

Pod tym względem powłoka będzie znacznie szybsza:

$ time sh -c 'seq 100000000' > /dev/null
sh -c 'seq 100000000' > /dev/null  0.77s user 0.03s system 99% cpu 0.805 total
$ time python -c 'i=0
> while i <= 100000000: i=i+1'
python -c 'i=0 while i <= 100000000: i=i+1'  12.12s user 0.00s system 99% cpu 12.127 total

Chociaż oczywiście większość zadań jest wykonywana przez polecenia wywoływane przez powłokę, tak jak powinno być.

Teraz możesz oczywiście zrobić to samo z python:

python -c '
import os
os.dup2(os.open("/dev/null", os.O_WRONLY), 1);
os.execlp("seq", "seq", "100000000")'

Ale tak naprawdę nie można tego robić, pythonponieważ pythonjest to przede wszystkim język programowania, a nie interpreter wiersza poleceń.

Pamiętaj, że możesz zrobić:

python -c 'import os; os.system("seq 100000000 > /dev/null")'

Ale pythontak naprawdę dzwoniłby do powłoki, aby zinterpretować ten wiersz poleceń!


Uwielbiam twoją odpowiedź. W wielu innych odpowiedziach omawiane są ulepszone techniki „jak”, podczas gdy ty obejmujesz zarówno „dlaczego”, jak i spostrzegawczo „dlaczego nie”, zajmując się błędem w metodyce podejścia PO.
greg.arnott



2

Oprócz komentarzy możesz trochę zoptymalizować kod , np

#!/bin/bash
for (( i = 0; i <= 1000000000; i++ ))
do
: # null command
done

Ten kod powinien zająć trochę mniej czasu.

Ale oczywiście nie jest wystarczająco szybki, aby można go było faktycznie wykorzystać.


-3

Zauważyłem dramatyczną różnicę w bash od użycia wyrażeń logicznie równoważnych „while” i „till”:

time (i=0 ; while ((i<900000)) ; do  i=$((i+1)) ; done )

real    0m5.339s
user    0m5.324s
sys 0m0.000s

time (i=0 ; until ((i=900000)) ; do  i=$((i+1)) ; done )

real    0m0.000s
user    0m0.000s
sys 0m0.000s

Nie dlatego, że tak naprawdę ma to ogromne znaczenie dla pytania, poza tym, że czasami małe różnice mogą mieć duże znaczenie, nawet jeśli spodziewalibyśmy się, że będą równoważne.


6
Spróbuj tego ((i==900000)).
Tomasz

2
Używasz =do zadania. To zwróci prawdę natychmiast. Nie nastąpi pętla.
Wildcard,

1
Czy używałeś wcześniej Bash? :)
LinuxSecurityFreak
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.