C'è qualcosa che non va nella mia sceneggiatura o Bash è molto più lento di Python?


29

Stavo testando la velocità di Bash e Python eseguendo un loop 1 miliardo di volte.

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

Codice Bash:

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

Usando il timecomando ho scoperto che il codice Python impiega solo 48 secondi per terminare mentre il codice Bash ha impiegato oltre 1 ora prima che uccidessi lo script.

Perché è così? Mi aspettavo che Bash sarebbe stato più veloce. C'è qualcosa di sbagliato nel mio script o Bash è davvero molto più lento con questo script?


49
Non sono del tutto sicuro del perché ti aspettassi che Bash fosse più veloce di Python.
Kusalananda

9
@MatijaNalis no non puoi! Lo script viene caricato in memoria, la modifica del file di testo da cui è stato letto (il file di script) non avrà alcun effetto sullo script in esecuzione. Inoltre, bash è già abbastanza lento senza dover aprire e rileggere un file ogni volta che viene eseguito un ciclo!
terdon


4
Bash legge il file riga per riga mentre viene eseguito, ma ricorda ciò che legge se arriva di nuovo a quella riga (perché è in un ciclo o in una funzione). L'affermazione originale sulla rilettura di ogni iterazione non è vera, ma le modifiche alle righe ancora da raggiungere saranno efficaci. Una dimostrazione interessante: crea un file contenente echo echo hello >> $0ed eseguilo.
Michael Homer,

3
@MatijaNalis ah, OK, posso capirlo. È stata l'idea di cambiare un anello da corsa che mi ha gettato. Presumibilmente, ogni riga viene letta in sequenza e solo dopo che l'ultima è terminata. Tuttavia, un ciclo viene trattato come un singolo comando e verrà letto nella sua interezza, quindi la modifica non influirà sul processo in esecuzione. Una distinzione interessante, però, avevo sempre supposto che l'intero script fosse caricato in memoria prima dell'esecuzione. Grazie per segnalarlo!
terdon

Risposte:


17

Questo è un bug noto in bash; vedi la pagina man e cerca "BUGS":

BUGS
       It's too big and too slow.

;)


Per un eccellente primer sulle differenze concettuali tra shell scripting e altri linguaggi di programmazione, consiglio vivamente di leggere:

Gli estratti più pertinenti:

Le conchiglie sono un linguaggio di livello superiore. Si potrebbe dire che non è nemmeno una lingua. Sono davanti a tutti gli interpreti della riga di comando. Il lavoro viene svolto da quei comandi che esegui e la shell ha il solo scopo di orchestrarli.

...

IOW, nelle shell, in particolare per elaborare il testo, invochi il minor numero possibile di utility e le fai cooperare all'attività, non esegui migliaia di strumenti in sequenza in attesa che ciascuno si avvii, venga eseguito, ripulito prima di eseguire il successivo.

...

Come detto in precedenza, l'esecuzione di un comando ha un costo. Un costo enorme se quel comando non è incorporato, ma anche se sono integrati, il costo è grande.

E le shell non sono state progettate per funzionare in questo modo, non hanno alcuna pretesa di essere linguaggi di programmazione performanti. Non lo sono, sono solo interpreti da riga di comando. Quindi, su questo fronte è stata fatta poca ottimizzazione.


Non usare grandi loop negli script di shell.


54

I loop di shell sono lenti e quelli di bash sono i più lenti. Le conchiglie non sono pensate per fare un lavoro pesante nei circuiti. Le shell hanno lo scopo di avviare alcuni processi esterni e ottimizzati su lotti di dati.


Ad ogni modo, ero curioso di confrontare i loop di shell, quindi ho fatto un piccolo punto di riferimento:

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

( Dettagli:

  • CPU: CPU Intel (R) Core (TM) i5 M 430 a 2,27 GHz
  • ksh: versione sh (Ricerca AT&T) 93u + 2012-08-01
  • bash: GNU bash, versione 4.3.11 (1) -release (x86_64-pc-linux-gnu)
  • zsh: zsh 5.2 (x86_64-unknown-linux-gnu)
  • trattino: 0,5,7-4ubuntu1

)

I risultati (abbreviati) (tempo per iterazione) sono:

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

Dai risultati:

Se vuoi un loop shell leggermente più veloce, allora se hai la [[sintassi e vuoi un loop shell veloce, sei in una shell avanzata e hai anche il C-like per loop. Usa quindi la C come per loop. Possono essere circa 2 volte più veloci di while [-loops nella stessa shell.

  • ksh ha il for (ciclo più veloce a circa 2,7 µs per iterazione
  • trattino ha il while [ciclo più veloce a circa 5,8 µs per iterazione

C per i loop può essere 3-4 ordini decimali di grandezza più veloci. (Ho sentito che i Torvald amano C).

Il ciclo C for ottimizzato è 56500 volte più veloce del while [loop di bash (il loop di shell più lento) e 6750 volte più veloce del for (loop di ksh (il loop di shell più veloce).


Ancora una volta, la lentezza delle shell non dovrebbe importare molto, perché il modello tipico con le shell è scaricare su alcuni processi di programmi esterni ottimizzati.

Con questo modello, le shell spesso rendono molto più semplice scrivere script con prestazioni superiori agli script Python (l'ultima volta che ho controllato, creare pipeline di processo in Python era piuttosto goffo).

Un'altra cosa da considerare è il tempo di avvio.

time python3 -c ' '

impiega da 30 a 40 ms sul mio PC mentre le shell impiegano circa 3ms. Se avvii molti script, questo si somma rapidamente e puoi fare molto nei 27-37 ms extra che python impiega solo per iniziare. Piccoli script possono essere finiti più volte nello stesso lasso di tempo.

(NodeJs è probabilmente il peggior runtime di scripting in questo dipartimento in quanto ci vogliono circa 100ms solo per iniziare (anche se una volta iniziato, sarebbe difficile trovare un performer migliore tra i linguaggi di scripting)).


Per ksh, è possibile specificare l'attuazione (AT & T ksh88, AT & T ksh93, pdksh, mksh...) in quanto non c'è un bel po 'di variazione tra loro. Per bash, potresti voler specificare la versione. Di recente ha fatto alcuni progressi (vale anche per altre shell).
Stéphane Chazelas,

@ StéphaneChazelas Grazie. Ho aggiunto le versioni del software e dell'hardware utilizzati.
PSkocik,

Per riferimento: per creare una pipeline processo in python che devi fare qualcosa di simile: from subprocess import *; p1=Popen(['echo', 'something'], stdout=PIPE); p2 = Popen(['grep', 'pattern'], stdin=p1.stdout, stdout=PIPE); Popen(['wc', '-c'], stdin=PIPE). Questo è davvero goffo, ma non dovrebbe essere difficile codificare una pipelinefunzione che lo fa per qualsiasi numero di processi, risultando pipeline(['echo', 'something'], ['grep', 'patter'], ['wc', '-c']).
Bakuriu,

1
Ho pensato che forse l'ottimizzatore gcc stava eliminando totalmente il ciclo. Non lo è, ma sta ancora facendo un'interessante ottimizzazione: utilizza le istruzioni SIMD per eseguire 4 aggiunte in parallelo, riducendo il numero di iterazioni di loop a 250000.
Mark Plotnick,

1
@PSkocik: è proprio al limite di ciò che gli ottimizzatori possono fare nel 2016. Sembra che C ++ 17 imporrà che i compilatori debbano essere in grado di calcolare espressioni simili al momento della compilazione (nemmeno come ottimizzazione). Con quella funzionalità C ++ attiva, GCC può prenderla anche come ottimizzazione per C.
Saluti

18

Ho fatto un po 'di test e sul mio sistema ho eseguito quanto segue: nessuno ha reso l'ordine di accelerazione di grandezza che sarebbe necessario per essere competitivo, ma puoi renderlo più veloce:

Test 1: 18.233s

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

La parte importante in quest'ultima è l'esportazione LC_ALL = C. Ho scoperto che molte operazioni bash finiscono in modo significativamente più veloce se viene utilizzato, in particolare qualsiasi funzione regex. Mostra anche un non documentato per la sintassi per usare {} e: come no-op.


3
+1 per il suggerimento LC_ALL, non lo sapevo.
einpoklum - ripristina Monica il

+1 Interessante come [[è molto più veloce di [. Non sapevo che LC_ALL = C (BTW non è necessario esportarlo) ha fatto la differenza.
PSkocik,

@PSkocik Per quanto ne so, [[è un built-in bash, ed [è davvero /bin/[, che è lo stesso di /bin/test- un programma esterno. Ecco perché è più lento.
tomsmeding

@tomsmending [è integrato in tutte le shell comuni (provare type [). Il programma esterno è per lo più inutilizzato ora.
PSkocik,

10

Una shell è efficiente se la usi per quello per cui è stata progettata (sebbene l'efficienza sia raramente ciò che cerchi in una shell).

Una shell è un interprete della riga di comando, è progettata per eseguire comandi e farli cooperare a un'attività.

Se si desidera contare al 1000000000, si richiama un comando (uno) per contare, come seq, bc, awko python/ perl... Esecuzione 1000000000 [[...]]comandi e 1000000000 letcomandi è destinato ad essere terribilmente inefficiente, in particolare con bashil quale è il guscio più lento di tutti.

A tal proposito, una shell sarà molto più veloce:

$ 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

Anche se, naturalmente, la maggior parte del lavoro viene eseguita dai comandi che la shell invoca, come dovrebbe essere.

Ora, ovviamente, potresti fare lo stesso con python:

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

Ma non è così che faresti le cose in pythonquanto pythonè principalmente un linguaggio di programmazione, non un interprete della riga di comando.

Nota che potresti fare:

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

Ma, in pythonrealtà, chiamerebbe una shell per interpretare quella riga di comando!


Adoro la tua risposta. Tante altre risposte discutono di tecniche "how" migliorate, mentre si tratta sia del "why" sia percettivamente del "why not" affrontando l'errore nella metodologia di approccio del PO.
Greg.arnott,



2

A parte i commenti, potresti ottimizzare un po ' il codice , ad es

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

Questo codice dovrebbe richiedere un po ' meno tempo.

Ma ovviamente non abbastanza veloce da essere effettivamente utilizzabile.


-3

Ho notato una notevole differenza in bash dall'uso delle espressioni "while" e "fino a" logicamente equivalenti:

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

Non che abbia davvero un'enorme rilevanza per la domanda, a parte il fatto che forse a volte piccole differenze fanno una grande differenza, anche se ci aspetteremmo che sarebbero equivalenti.


6
Prova con questo ((i==900000)).
Tomasz,

2
Stai usando =per un incarico. Tornerà vero immediatamente. Non si verificherà alcun loop.
Wildcard il

1
Hai mai usato Bash prima? :)
LinuxSecurityFreak
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.