¿Hay algún problema con mi script o Bash es mucho más lento que Python?

29

Estaba probando la velocidad de Bash y Python ejecutando un bucle mil millones de veces.

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

Código bash:

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

Usando el timecomando descubrí que el código de Python tarda solo 48 segundos en finalizar, mientras que el código de Bash tardó más de 1 hora antes de que elimine el script.

¿Por qué esto es tan? Esperaba que Bash fuera más rápido. ¿Hay algún problema con mi script o Bash es realmente mucho más lento con este script?

Edward Torvalds
fuente
49
No estoy muy seguro de por qué esperabas que Bash fuera más rápido que Python.
Kusalananda
9
@MatijaNalis no, no puedes! El script se carga en la memoria, la edición del archivo de texto del que se leyó (el archivo de script) no tendrá absolutamente ningún efecto en el script en ejecución. También es bueno que bash sea lo suficientemente lento sin tener que abrir y volver a leer un archivo cada vez que se ejecuta un bucle.
terdon
44
Bash lee el archivo línea por línea a medida que se ejecuta, pero recuerda lo que lee si llega a esa línea nuevamente (porque está en un bucle o una función). El reclamo original sobre la relectura de cada iteración no es cierto, pero las modificaciones a las líneas aún por alcanzar serán efectivas. Una demostración interesante: crea un archivo que lo contenga echo echo hello >> $0y ejecútalo.
Michael Homer
3
@MatijaNalis ah, está bien, puedo entender eso. Fue la idea de cambiar un ciclo de ejecución lo que me arrojó. Presumiblemente, cada línea se lee secuencialmente y solo después de que la última haya terminado. Sin embargo, un bucle se trata como un comando único y se leerá en su totalidad, por lo que cambiarlo no afectará el proceso en ejecución. Sin embargo, una distinción interesante es que siempre asumí que todo el script se carga en la memoria antes de la ejecución. ¡Gracias por mencionarlo!
terdon

Respuestas:

17

Este es un error conocido en bash; vea la página de manual y busque "BUGS":

BUGS
       It's too big and too slow.

;)


Para una excelente introducción sobre las diferencias conceptuales entre los scripts de shell y otros lenguajes de programación, recomiendo leer:

Los extractos más pertinentes:

Los shells son un lenguaje de nivel superior. Se puede decir que ni siquiera es un idioma. Están ante todos los intérpretes de línea de comandos. El trabajo lo realizan los comandos que ejecuta y el shell solo está destinado a orquestarlos.

...

IOW, en shells, especialmente para procesar texto, invocas la menor cantidad de utilidades posible y haces que cooperen en la tarea, no ejecutan miles de herramientas en secuencia esperando que cada una comience, se ejecute y se limpie antes de ejecutar la siguiente.

...

Como se dijo anteriormente, ejecutar un comando tiene un costo. Un costo enorme si ese comando no está integrado, pero incluso si están integrados, el costo es grande.

Y los shells no han sido diseñados para ejecutarse así, no tienen pretensiones de ser lenguajes de programación eficaces. No lo son, solo son intérpretes de línea de comandos. Por lo tanto, se ha realizado poca optimización en este frente.


No use bucles grandes en las secuencias de comandos de shell.

Comodín
fuente
54

Los bucles de shell son lentos y bash son los más lentos. Los proyectiles no están destinados a realizar trabajos pesados ​​en bucles. Los shells están destinados a lanzar algunos procesos externos optimizados en lotes de datos.


De todos modos, tenía curiosidad por cómo se comparan los bucles de shell, así que hice un pequeño punto de referencia:

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

( Detalles:

  • CPU: Intel (R) Core (TM) i5 CPU M 430 @ 2.27GHz
  • ksh: versión sh (AT&T Research) 93u + 2012-08-01
  • bash: GNU bash, versión 4.3.11 (1) -release (x86_64-pc-linux-gnu)
  • zsh: zsh 5.2 (x86_64-unknown-linux-gnu)
  • guión: 0.5.7-4ubuntu1

)

Los resultados (abreviados) (tiempo por iteración) son:

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

De los resultados:

Si desea un bucle de shell un poco más rápido, entonces si tiene la [[sintaxis y desea un bucle de shell rápido, está en un shell avanzado y también tiene el bucle de tipo C. Usa el me gusta C para el ciclo, entonces. Pueden ser aproximadamente 2 veces más rápidos que los while [bucles en el mismo shell.

  • ksh tiene el for (ciclo más rápido a aproximadamente 2.7 µs por iteración
  • el guión tiene el while [bucle más rápido a aproximadamente 5.8 µs por iteración

C para los bucles puede ser de 3 a 4 órdenes decimales de magnitud más rápido. (Escuché que los Torvalds aman a C).

El C for loop optimizado es 56500 veces más rápido que el while [bucle de bash (el bucle de shell más lento) y 6750 veces más rápido que el for (bucle de ksh (el bucle de shell más rápido).


Una vez más, la lentitud de los shells no debería importar mucho, porque el patrón típico con los shells es descargarse a unos pocos procesos de programas externos optimizados.

Con este patrón, los shells a menudo hacen que sea mucho más fácil escribir scripts con un rendimiento superior a los scripts de Python (la última vez que lo verifiqué, la creación de canales de proceso en Python fue bastante torpe).

Otra cosa a considerar es el tiempo de inicio.

time python3 -c ' '

tarda de 30 a 40 ms en mi PC, mientras que los shells tardan alrededor de 3 ms. Si ejecuta una gran cantidad de scripts, esto se suma rápidamente y puede hacer mucho en los 27-37 ms adicionales que Python tarda solo para comenzar. Las secuencias de comandos pequeñas se pueden terminar varias veces en ese período de tiempo.

(NodeJs es probablemente el peor tiempo de ejecución de secuencias de comandos en este departamento, ya que se necesitan unos 100 ms para comenzar (aunque una vez que haya comenzado, sería difícil encontrar un mejor rendimiento entre los lenguajes de secuencias de comandos)).

PSkocik
fuente
Para ksh, es posible que desee especificar la aplicación (AT & T ksh88, AT & T ksh93, pdksh, mksh...) ya que hay un buen montón de variación entre ellos. Para bash, es posible que desee especificar la versión. Hizo algunos progresos últimamente (eso se aplica también a otros proyectiles).
Stéphane Chazelas
@ StéphaneChazelas Gracias. Agregué las versiones del software y hardware utilizados.
PSkocik
Como referencia: la creación de una tubería de proceso en Python que tiene que hacer algo como: from subprocess import *; p1=Popen(['echo', 'something'], stdout=PIPE); p2 = Popen(['grep', 'pattern'], stdin=p1.stdout, stdout=PIPE); Popen(['wc', '-c'], stdin=PIPE). Esto es realmente torpe, pero no debería ser difícil codificar una pipelinefunción que hace esto por usted para cualquier número de procesos, lo que resulta en pipeline(['echo', 'something'], ['grep', 'patter'], ['wc', '-c']).
Bakuriu
1
Pensé que tal vez el optimizador gcc estaba eliminando totalmente el bucle. No lo es, pero todavía está haciendo una optimización interesante: usa instrucciones SIMD para hacer 4 adiciones en paralelo, reduciendo el número de iteraciones de bucle a 250000.
Mark Plotnick
1
@PSkocik: está al borde de lo que los optimizadores pueden hacer en 2016. Parece que C ++ 17 exigirá que los compiladores puedan calcular expresiones similares en el momento de la compilación (ni siquiera como una optimización). Con esa capacidad de C ++ en su lugar, GCC puede recogerlo como una optimización para C también.
MSalters
18

Hice un poco de prueba, y en mi sistema ejecuté lo siguiente: ninguno hizo el orden de aceleración de magnitud que sería necesario para ser competitivo, pero puede hacerlo más rápido:

Prueba 1: 18.233s

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

prueba2: 20.45s

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

prueba3: 17.64s

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

prueba4: 26.69s

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

prueba5: 12.79s

#!/bin/bash
export LC_ALL=C

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

La parte importante en este último es la exportación LC_ALL = C. He descubierto que muchas operaciones de bash terminan significativamente más rápido si se usa esto, en particular cualquier función regex. También muestra una sintaxis indocumentada para usar {} y: como un no-op.

Erik Brandsberg
fuente
3
+1 para la sugerencia LC_ALL, no lo sabía.
einpoklum - reinstalar a Mónica el
+1 Interesante porque [[es mucho más rápido que [. No sabía que LC_ALL = C (por cierto, no necesita exportarlo) hizo la diferencia.
PSkocik
@PSkocik Hasta donde yo sé, [[es un bash incorporado, y [realmente /bin/[es lo mismo que /bin/testun programa externo. Por eso es más lento.
tomsmeding
@tomsmending [está integrado en todos los shells comunes (prueba type [). El programa externo está actualmente sin usar.
PSkocik
10

Un shell es eficiente si lo usa para lo que ha sido diseñado (aunque la eficiencia rara vez es lo que busca en un shell).

Un shell es un intérprete de línea de comandos, está diseñado para ejecutar comandos y hacer que cooperen en una tarea.

Si desea contar hasta mil millones, se invoca un (una) de comandos para contar, como seq, bc, awko python/ perl... Correr 1000000000 [[...]]comandos y 1000000000 letcomandos está destinada a ser terriblemente ineficiente, especialmente en lo bashque es la capa más lento de todos.

En ese sentido, un shell será mucho más rápido:

$ 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

Aunque, por supuesto, la mayor parte del trabajo se realiza mediante los comandos que invoca el shell, como debería ser.

Ahora, por supuesto, podría hacer lo mismo con python:

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

Pero esa no es realmente la forma de hacer las cosas, pythonya que pythones principalmente un lenguaje de programación, no un intérprete de línea de comandos.

Tenga en cuenta que puede hacer:

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

¡Pero, en pythonrealidad estaría llamando a un shell para interpretar esa línea de comando!

Stéphane Chazelas
fuente
Me encanta tu respuesta Muchas otras respuestas discuten técnicas mejoradas de "cómo", mientras que cubre tanto el "por qué" como el "por qué no" perceptivamente abordando el error en la metodología de enfoque del PO.
greg.arnott
3

Nada está mal (excepto sus expectativas) ya que Python es bastante rápido para un lenguaje no compilado, consulte https://wiki.python.org/moin/PythonSpeed

Matija Nalis
fuente
1
Prefiero disuadir de respuestas como esta, esto pertenece a los comentarios en mi humilde opinión.
LinuxSecurityFreak
2

Aparte de los comentarios, puede optimizar un poco el código , por ejemplo

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

Este código debería tomar un poco menos de tiempo.

Pero obviamente no es lo suficientemente rápido como para ser realmente utilizable.

LinuxSecurityFreak
fuente
-3

He notado una diferencia dramática en bash del uso de expresiones "mientras" y "hasta" lógicamente equivalentes:

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

No es que realmente tenga una relevancia tremenda para la pregunta, aparte de eso, a veces, las pequeñas diferencias pueden marcar una gran diferencia, a pesar de que esperaríamos que fueran equivalentes.

pingüino intrépido
fuente
66
Intenta con este ((i==900000)).
Tomasz
2
Estás usando =para la asignación. Volverá verdadero inmediatamente. No se realizará ningún bucle.
Comodín el
1
¿Realmente has usado Bash antes? :)
LinuxSecurityFreak