Stimmt etwas mit meinem Skript nicht oder ist Bash viel langsamer als Python?

29

Ich habe die Geschwindigkeit von Bash und Python getestet, indem ich 1 Milliarde Mal eine Schleife ausgeführt habe.

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

Bash-Code:

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

Mit dem timeBefehl habe ich herausgefunden, dass der Python-Code nur 48 Sekunden benötigt, während der Bash-Code über 1 Stunde dauerte, bevor ich das Skript beendet habe.

Warum ist das so? Ich erwartete, dass Bash schneller sein würde. Stimmt etwas mit meinem Skript nicht oder ist Bash mit diesem Skript wirklich viel langsamer?

Edward Torvalds
quelle
49
Ich bin mir nicht ganz sicher, warum Sie erwartet haben, dass Bash schneller ist als Python.
Kusalananda
9
@MatijaNalis nein du kannst nicht! Das Skript wird in den Speicher geladen. Das Bearbeiten der Textdatei, aus der es gelesen wurde (die Skriptdatei), hat keinerlei Auswirkungen auf das ausgeführte Skript. Eine gute Sache ist auch, dass Bash bereits langsam genug ist, ohne dass eine Datei jedes Mal geöffnet und erneut gelesen werden muss, wenn eine Schleife ausgeführt wird!
Terdon
4
Bash liest die Datei zeilenweise, während sie ausgeführt wird, merkt sich jedoch, was sie gelesen hat, wenn sie erneut in diese Zeile kommt (weil sie sich in einer Schleife oder einer Funktion befindet). Die ursprüngliche Behauptung über das erneute Lesen jeder Iteration ist nicht wahr, aber Änderungen an noch zu erreichenden Zeilen sind wirksam. Eine interessante Demonstration: Erstellen Sie eine Datei mit echo echo hello >> $0und führen Sie sie aus.
Michael Homer
3
@MatijaNalis ah, OK, das kann ich verstehen. Es war die Idee, eine Laufschleife zu wechseln, die mich umwarf. Vermutlich wird jede Zeile nacheinander und erst nach Beendigung der letzten Zeile gelesen. Eine Schleife wird jedoch als einzelner Befehl behandelt und vollständig gelesen, sodass Änderungen den laufenden Prozess nicht beeinflussen. Interessante Unterscheidung, ich hatte immer angenommen, dass das gesamte Skript vor der Ausführung in den Speicher geladen wird. Vielen Dank für den Hinweis!
Terdon

Antworten:

17

Dies ist ein bekannter Fehler in der Bash. Siehe die Manpage und suche nach "BUGS":

BUGS
       It's too big and too slow.

;)


Für eine exzellente Einführung in die konzeptionellen Unterschiede zwischen Shell-Scripting und anderen Programmiersprachen empfehle ich dringend, Folgendes zu lesen:

Die zutreffendsten Auszüge:

Muscheln sind eine höhere Sprache. Man kann sagen, es ist nicht einmal eine Sprache. Sie stehen vor allen Befehlszeileninterpreten. Die Aufgabe wird von den Befehlen erledigt, die Sie ausführen, und die Shell soll sie nur orchestrieren.

...

In Shells, insbesondere zum Verarbeiten von Text, rufen Sie so wenige Dienstprogramme wie möglich auf und lassen sie bei der Ausführung der Aufgabe zusammenarbeiten. Führen Sie nicht Tausende von Tools nacheinander aus, während Sie darauf warten, dass jedes gestartet, ausgeführt und bereinigt wird, bevor Sie das nächste ausführen.

...

Wie bereits erwähnt, ist das Ausführen eines Befehls mit Kosten verbunden. Ein enormer Aufwand, wenn dieser Befehl nicht eingebaut ist, aber selbst wenn er eingebaut ist, sind die Kosten hoch.

Und Shells sind nicht so konzipiert, sie geben keinen Anspruch darauf, performante Programmiersprachen zu sein. Sie sind nicht, sie sind nur Befehlszeileninterpreter. In dieser Hinsicht wurde wenig optimiert.


Verwenden Sie in Shell-Skripten keine großen Schleifen.

Platzhalter
quelle
54

Shell-Loops sind langsam und Bashs sind die langsamsten. Muscheln sind nicht dazu gedacht, schwere Arbeit in Schleifen zu verrichten. Shells sollen einige externe, optimierte Prozesse für Datenstapel starten.


Wie auch immer, ich war neugierig, wie sich Shell-Loops vergleichen lassen, also habe ich einen kleinen Benchmark gemacht:

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

( Details:

  • CPU: Intel (R) Core (TM) i5-CPU M 430 bei 2,27 GHz
  • ksh: version sh (AT & T Research) 93u + 01.08.2012
  • bash: GNU bash, Version 4.3.11 (1) -release (x86_64-pc-linux-gnu)
  • zsh: zsh 5.2 (x86_64-unknown-linux-gnu)
  • Bindestrich: 0.5.7-4ubuntu1

)

Die (abgekürzten) Ergebnisse (Zeit pro Iteration) sind:

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

Aus den Ergebnissen:

Wenn Sie eine etwas schnellere Shell-Schleife wünschen, dann befinden Sie sich [[in einer erweiterten Shell und haben auch die C-ähnliche for-Schleife , wenn Sie die Syntax und die schnelle Shell-Schleife wünschen. Verwenden Sie dann die C like for-Schleife. Sie können ungefähr 2-mal so schnell sein wie while [-loops in derselben Shell.

  • ksh hat die schnellste for (Schleife bei etwa 2,7 us pro Iteration
  • dash hat die schnellste while [Schleife mit ca. 5,8 µs pro Iteration

C für Schleifen kann 3-4 Dezimalstellen schneller sein. (Ich habe gehört, die Torvalds lieben C).

Die optimierte C for -Schleife ist 56500-mal schneller als die bash- while [Schleife (die langsamste Shell-Schleife) und 6750-mal schneller als die ksh- for (Schleife (die schnellste Shell-Schleife).


Auch hier sollte die Langsamkeit von Shells keine Rolle spielen, da das typische Muster bei Shells darin besteht, einige Prozesse von externen, optimierten Programmen auszulagern.

Mit diesem Muster machen es Shells oft einfacher, Skripte mit einer Leistung zu schreiben, die Python-Skripten überlegen ist.

Eine weitere zu berücksichtigende Sache ist die Startzeit.

time python3 -c ' '

dauert 30 bis 40 ms auf meinem PC, während Shells ca. 3 ms dauern. Wenn Sie viele Skripte starten, summiert sich dies schnell und Sie können sehr viel in den zusätzlichen 27-37 ms tun, die Python benötigt, um zu starten. Kleine Skripte können in diesem Zeitraum mehrmals abgearbeitet werden.

(NodeJs ist wahrscheinlich die schlechteste Skriptlaufzeit in dieser Abteilung, da der Start nur etwa 100 ms dauert (auch wenn es einmal gestartet ist, wird es Ihnen schwer fallen, unter den Skriptsprachen einen besseren Interpreten zu finden).

PSkocik
quelle
Für KSH, können Sie die Implementierung (AT & T angeben ksh88, AT & T ksh93, pdksh, mksh...) , denn es gibt eine ganze Menge von Variation zwischen ihnen. Für bashmöchten Sie möglicherweise die Version angeben. Es hat in letzter Zeit einige Fortschritte gemacht (das gilt auch für andere Shells).
Stéphane Chazelas
@ StéphaneChazelas Danke. Ich habe die Versionen der verwendeten Software und Hardware hinzugefügt.
PSkocik
Als Referenz: eine Prozess - Pipeline in Python erstellen Sie so etwas wie zu tun haben: from subprocess import *; p1=Popen(['echo', 'something'], stdout=PIPE); p2 = Popen(['grep', 'pattern'], stdin=p1.stdout, stdout=PIPE); Popen(['wc', '-c'], stdin=PIPE). Dies ist in der Tat umständlich, aber es sollte nicht schwierig sein, eine pipelineFunktion zu programmieren , die dies für eine beliebige Anzahl von Prozessen erledigt, was dazu führt pipeline(['echo', 'something'], ['grep', 'patter'], ['wc', '-c']).
Bakuriu
1
Ich dachte, dass der GCC-Optimierer die Schleife möglicherweise vollständig beseitigt. Dies ist nicht der Fall, aber es wird immer noch eine interessante Optimierung durchgeführt: Mithilfe von SIMD-Anweisungen werden 4 Additionen parallel ausgeführt, wodurch die Anzahl der Schleifeniterationen auf 250000 verringert wird.
Mark Plotnick,
1
@PSkocik: Es steht kurz davor, was Optimierer in 2016 tun können. Offenbar wird C ++ 17 vorschreiben, dass Compiler ähnliche Ausdrücke zur Kompilierungszeit berechnen können müssen (nicht einmal als Optimierung). Mit dieser C ++ - Fähigkeit kann GCC sie auch als Optimierung für C aufgreifen.
MSalters
18

Ich habe ein paar Tests durchgeführt und auf meinem System Folgendes ausgeführt: Keiner hat die Größenordnung der Geschwindigkeit erreicht, die erforderlich wäre, um wettbewerbsfähig zu sein, aber Sie können es schneller machen:

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

Test 3: 17,64 s

#!/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++)) { 
:
}

Der wichtige Teil in diesem letzten ist der Export LC_ALL = C. Ich habe festgestellt, dass viele Bash-Operationen erheblich schneller enden, wenn dies verwendet wird, insbesondere jede Regex-Funktion. Es zeigt auch eine undokumentierte Syntax für die Verwendung von {} und: als No-Op.

Erik Brandsberg
quelle
3
+1 für den LC_ALL-Vorschlag, das wusste ich nicht.
einpoklum - wieder Monica
+1 Interessant wie [[viel schneller das ist als [. Ich wusste nicht, dass LC_ALL = C (übrigens müssen Sie es nicht exportieren) einen Unterschied macht.
PSkocik
@PSkocik Soweit ich weiß, [[ist eine Bash eingebaut, und das [ist wirklich /bin/[dasselbe wie /bin/test- ein externes Programm. Deshalb ist sie langsamer.
Tomsmeding
@tomsmending [ist in alle gängigen Shells integriert (try type [). Das externe Programm wird derzeit größtenteils nicht verwendet.
PSkocik
10

Eine Shell ist effizient, wenn Sie sie für das verwenden, wofür sie entwickelt wurde (obwohl Effizienz selten das ist, wonach Sie in einer Shell suchen).

Eine Shell ist ein Befehlszeileninterpreter, mit dem Befehle ausgeführt und bei einer Aufgabe zusammengearbeitet werden können.

Wenn Sie auf eine Milliarde zählen möchten, rufen Sie einen (ein) Befehl zu zählen, wie seq, bc, awkoder python/ perl... Lauf 1000000000 [[...]]Befehle und 1000000000 letBefehle gebunden ist schrecklich ineffizient sein, vor allem mit bashdem die langsamste Schale von allen ist.

In dieser Hinsicht ist eine Shell viel schneller:

$ 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

Natürlich wird der größte Teil der Arbeit von den Befehlen erledigt, die die Shell aufruft, wie es sein sollte.

Nun können Sie natürlich dasselbe tun mit python:

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

Aber so würde man es nicht machen, pythonda pythones sich in erster Linie um eine Programmiersprache und nicht um einen Befehlszeileninterpreter handelt.

Beachten Sie, dass Sie Folgendes tun können:

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

Aber pythonwürde tatsächlich eine Shell aufrufen, um diese Befehlszeile zu interpretieren!

Stéphane Chazelas
quelle
Ich liebe deine Antwort. So viele andere Antworten diskutieren verbesserte "Wie" -Techniken, während Sie sowohl das "Warum" als auch das "Warum nicht" behandeln, um den Fehler in der Methodik des Vorgehens des OP zu beheben.
greg.arnott
3

Nichts ist falsch (außer Ihren Erwartungen), da Python für nicht kompilierte Sprachen sehr schnell ist (siehe https://wiki.python.org/moin/PythonSpeed)

Matija Nalis
quelle
1
Ich rate eher von solchen Antworten ab, dies gehört meiner Meinung nach zu den Kommentaren.
LinuxSecurityFreak
2

Neben den Kommentaren können Sie den Code ein wenig optimieren , z

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

Dieser Code sollte etwas kürzer sein.

Aber offensichtlich nicht schnell genug, um tatsächlich brauchbar zu sein.

LinuxSecurityFreak
quelle
-3

Ich habe einen dramatischen Unterschied zwischen der Verwendung von logisch äquivalenten "while" - und "until" -Ausdrücken festgestellt:

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

Nicht, dass es wirklich eine enorme Relevanz für die Frage hat, ansonsten machen vielleicht manchmal kleine Unterschiede einen großen Unterschied, obwohl wir erwarten würden, dass sie gleichwertig sind.

unerschrockener Pinguin
quelle
6
Versuchen Sie es mit diesem ((i==900000)).
Tomasz
2
Sie verwenden =für die Zuordnung. Es wird sofort true zurückgegeben. Es findet keine Schleife statt.
Wildcard
1
Haben Sie Bash schon einmal benutzt? :)
LinuxSecurityFreak