Há algo errado com o meu script ou Bash é muito mais lento que o Python?

28

Eu estava testando a velocidade do Bash e do Python executando um loop de 1 bilhão de vezes.

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

Código de bash:

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

Usando o comando time , descobri que o código do Python leva apenas 48 segundos para ser concluído, enquanto o código do Bash demorou mais de uma hora antes de eu matar o script.

Por que isso é assim? Eu esperava que o Bash fosse mais rápido. Há algo errado com o meu script ou Bash é realmente muito mais lento com este script?

    
por Edward Torvalds 13.08.2016 / 10:43

8 respostas

16

Este é um bug conhecido no bash; veja a página de manual e procure por "BUGS":

BUGS
       It's too big and too slow.

;)

Para um excelente resumo sobre as diferenças conceituais entre scripts de shell e outras linguagens de programação, eu recomendo a leitura:

Os trechos mais pertinentes:

Shells are a higher level language. One may say it's not even a language. They're before all command line interpreters. The job is done by those commands you run and the shell is only meant to orchestrate them.

...

IOW, in shells, especially to process text, you invoke as few utilities as possible and have them cooperate to the task, not run thousands of tools in sequence waiting for each one to start, run, clean up before running the next one.

...

As said earlier, running one command has a cost. A huge cost if that command is not builtin, but even if they are builtin, the cost is big.

And shells have not been designed to run like that, they have no pretension to being performant programming languages. They are not, they're just command line interpreters. So, little optimisation has been done on this front.

Não use grandes loops em scripts de shell.

    
por 14.08.2016 / 22:16
53

Loops de shell são lentos e os de bash são os mais lentos. As conchas não são destinadas a fazer trabalhos pesados em loops. Os shells servem para lançar alguns processos externos otimizados em lotes de dados.

De qualquer forma, eu estava curioso para saber como os loops de shell são comparados, então fiz uma pequena referência:

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

( Detalhes:

  • CPU: CPU Intel (R) Core (TM) i5 M 430 @ 2,27 GHz
  • ksh: versão sh (Pesquisa AT & T) 93u + 2012-08-01
  • bash: GNU bash, versão 4.3.11 (1) -release (x86_64-pc-linux-gnu)
  • zsh: zsh 5.2 (x86_64-desconhecido-linux-gnu)
  • traço: 0.5.7-4ubuntu1

Os resultados (abreviados) (tempo por iteração) são:

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

Dos resultados:

Se você quiser um loop de shell um pouco mais rápido, se tiver a sintaxe [[ e desejar um loop de shell rápido, estará em um shell avançado e também terá o loop C para loop. Use o C como loop, então. Eles podem ser cerca de 2 vezes mais rápidos que while [ -loops no mesmo shell.

  • O ksh tem o loop for ( mais rápido em torno de 2,7µs por iteração
  • traço tem o loop while [ mais rápido em torno de 5.8µs por iteração

C para loops pode ser de 3 a 4 ordens de magnitude decimal mais rápido. (Eu ouvi os Torvalds amam C).

O loop C for otimizado é 56500 vezes mais rápido que o loop while [ do bash (o loop de shell mais lento) e 6750 vezes mais rápido que o loop for ( do ksh (o loop de shell mais rápido).

Mais uma vez, a lentidão das shells não deve importar muito, porque o padrão típico com shells é descarregar para alguns processos de programas externos e otimizados.

Com esse padrão, os shells geralmente tornam muito mais fácil escrever scripts com desempenho superior aos scripts python (da última vez que verifiquei, criar pipelines de processo em python era bastante desajeitado).

Outra coisa a considerar é o tempo de inicialização.

time python3 -c ' '

leva de 30 a 40 ms no meu PC, enquanto que os shells demoram cerca de 3ms. Se você lançar muitos scripts, isso se soma rapidamente e você pode fazer muito nos 27 a 37 ms que o python leva apenas para iniciar. Pequenos scripts podem ser concluídos várias vezes nesse período de tempo.

(NodeJs é provavelmente o pior tempo de execução de script neste departamento, já que leva apenas cerca de 100ms para ser iniciado (embora uma vez tenha sido iniciado, seria difícil encontrar um melhor desempenho entre as linguagens de script).

    
por 13.08.2016 / 11:33
18

Testei um pouco e, no meu sistema, executei o seguinte: nenhum fez a aceleração na ordem de grandeza necessária para ser competitiva, mas você pode torná-lo mais rápido:

Teste 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++)) { 
:
}

A parte importante neste último é a exportação LC_ALL = C. Descobri que muitas operações bash terminam significativamente mais rápidas se isso for usado, em particular qualquer função regex. Ele também mostra uma sintaxe não documentada para usar o {} e o: como um não-op.

    
por 13.08.2016 / 18:37
10

Um shell é eficiente se você usá-lo para o que foi projetado (embora a eficiência raramente seja o que você procura em um shell).

Um shell é um interpretador de linha de comando, ele é projetado para executar comandos e fazer com que eles cooperem com uma tarefa.

Se você quiser contar para 1000000000, invoque um comando (um) para contar, como seq , bc , awk ou python / perl ... Execução de 1000000000 [[...]] de comandos e 1000000000 let comandos são terrivelmente ineficientes, especialmente com bash , que é o shell mais lento de todos.

A esse respeito, um shell será muito mais 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

Embora, obviamente, a maior parte do trabalho seja feita pelos comandos que o shell invoca, como deveria ser.

Agora, você pode fazer o mesmo com python :

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

Mas não é exatamente assim que você faria as coisas em python , pois python é basicamente uma linguagem de programação, não um interpretador de linha de comando.

Note que você poderia fazer:

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

Mas, python estaria realmente chamando um shell para interpretar essa linha de comando!

    
por 14.08.2016 / 14:58
7

Resposta: o Bash é muito mais lento que o Python.

Um pequeno exemplo é na postagem do blog Desempenho de vários idiomas .

    
por 13.08.2016 / 11:02
3

Nada está errado (exceto suas expectativas), pois o Python é realmente muito rápido para a linguagem não-compilada, veja link

    
por 13.08.2016 / 10:59
2

Além dos comentários, você pode otimizar o código um pouco , por exemplo

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

Esse código deve levar um pouco menos tempo.

Mas obviamente não é rápido o suficiente para ser realmente utilizável.

    
por 13.08.2016 / 11:25
-3

Eu notei uma diferença dramática no bash do uso de expressões "while" e "until" logicamente 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
Não que realmente tenha uma relevância tremenda para a questão, a não ser que, às vezes, pequenas diferenças possam fazer uma grande diferença, mesmo que esperemos que elas sejam equivalentes.

    
por 15.08.2016 / 02:23

Tags