python vs bc na avaliação 6 ^ 6 ^ 6

29

Estou avaliando a expressão 6^6^6 usando python e bc separadamente.

O conteúdo do arquivo python é print 6**6**6 . Quando executo time python test.py , obtenho a saída como

real        0m0.067s
user        0m0.050s
sys         0m0.011s

E então, eu corri o comando time echo 6^6^6 | bc que me deu a seguinte saída

real        0m0.205s
user        0m0.197s
sys         0m0.005s

A partir desses resultados, fica claro que o tempo de sys tomado por python e bc foi 11ms e 5ms, respectivamente. O comando bc superou o python no nível de tempo sys , mas quando se trata de usuário e python em tempo real foi quase 4 vezes mais rápido que bc . O que poderia ter ido lá? Eu não dei prioridade aos processos como tal. Estou tentando entender essa situação.

    
por ganessh 21.02.2014 / 18:21

4 respostas

25

O Python importa um grande número de arquivos na inicialização:

% python -c 'import sys; print len(sys.modules)'
39

Cada um deles requer um número ainda maior de tentativas de abrir um arquivo Python, porque há muitas maneiras de definir um módulo:

% python -vv -c 'pass'
# installing zipimport hook
import zipimport # builtin
# installed zipimport hook
# trying site.so
# trying sitemodule.so
# trying site.py
# trying site.pyc
# trying /System/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/site.so
# trying /System/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/sitemodule.so
# trying /System/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/site.py
# /System/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/site.pyc matches /System/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/site.py
import site # precompiled from /System/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/site.pyc
# trying os.so
# trying osmodule.so
# trying os.py
# trying os.pyc
# trying /System/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/os.so
# trying /System/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/osmodule.so
# trying /System/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/os.py
# /System/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/os.pyc matches /System/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/os.py
import os # precompiled from /System/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/os.pyc
    ...

Cada "tentativa", exceto aquelas que são incorporadas, requer chamadas do sistema em nível de sistema operacional, e cada "importação" parece acionar cerca de 8 mensagens "que estão tentando". (Havia maneiras de reduzir isso usando zipimport, e cada caminho em seu PYTHONPATH pode exigir outra chamada.)

Isso significa que há quase 200 chamadas de sistema stat antes do Python iniciar na minha máquina, e "time" atribui isso a "sys" ao invés de "user", porque o programa do usuário está aguardando o sistema para fazer as coisas. p>

Em comparação, e como o terdon disse, "bc" não tem esse custo inicial alto. Olhando para a saída dtruss (eu tenho um Mac; "strace" para um sistema operacional baseado em Linux), vejo que bc não faz nenhuma chamada de sistema open () ou stat (), exceto por carregar alguns compartilhados as bibliotecas são o começo, o que é claro que o Python também faz. Além disso, o Python tem mais arquivos para ler, antes de estar pronto para processar qualquer coisa.

Esperar pelo disco é lento.

Você pode ter uma noção do custo de inicialização do Python fazendo:

time python -c pass

São 0,032s na minha máquina, enquanto 'print 6 ** 6 ** 6' é 0,072s, então o custo de inicialização é 1/2 do tempo total e o cálculo + conversão em decimal é a outra metade. Enquanto:

time echo 1 | bc

leva 0,005s, e "6 ^ 6 ^ 6" leva 0,184s, então a exponenciação do bc é 4x mais lenta que a do Python mesmo que seja 7x mais rápida para começar.

    
por 21.02.2014 / 19:02
11

Encontrei uma resposta em SO, explicando os diferentes campos:

  • Real is wall clock time - time from start to finish of the call. This is all elapsed time including time slices used by other processes and time the process spends blocked (for example if it is waiting for I/O to complete).

  • User is the amount of CPU time spent in user-mode code (outside the kernel) within the process. This is only actual CPU time used in executing the process. Other processes and time the process spends blocked do not count towards this figure.

  • Sys is the amount of CPU time spent in the kernel within the process. This means executing CPU time spent in system calls within the kernel, as opposed to library code, which is still running in user-space. Like 'user', this is only CPU time used by the process. See below for a brief description of kernel mode (also known as 'supervisor' mode) and the system call mechanism.

Assim, no seu exemplo específico, a versão do Python é mais rápida em termos do tempo real necessário para concluir. No entanto, a abordagem python gasta mais tempo no espaço do kernel, fazendo chamadas para as funções do kernel. O comando bc geralmente não gasta tempo no espaço do kernel e todo o tempo é gasto no espaço do usuário, presumivelmente executando o código bc interno.

Isso não faz diferença para você, a única informação que realmente importa é real , que é o tempo real entre o lançamento do comando e a saída.

Você também deve estar ciente de que essas minúsculas diferenças não são estáveis, elas também dependerão da carga do seu sistema e serão alteradas toda vez que você executar o comando:

$ for i in {1..10}; do ( time python test.py > /dev/null ) 2>&1; done | grep user
user    0m0.056s
user    0m0.052s
user    0m0.052s
user    0m0.052s
user    0m0.060s
user    0m0.052s
user    0m0.052s
user    0m0.056s
user    0m0.048s
user    0m0.056s

$ for i in {1..10}; do ( time echo 6^6^6 | bc > /dev/null ) 2>&1; done | grep user
user    0m0.188s
user    0m0.188s
user    0m0.176s
user    0m0.176s
user    0m0.172s
user    0m0.176s
user    0m0.180s
user    0m0.172s
user    0m0.172s
user    0m0.172s
    
por 21.02.2014 / 18:40
10

Vou explicar de outra perspectiva.

Para ser justo, bc tem vantagem, pois não precisa ler nada do disco e precisa apenas de seus blob / binários, enquanto o python precisa importar uma série de módulos + ler um arquivo. Portanto, seu teste pode ser direcionado para bc . Para testá-lo, você deve usar bc -q file , em que file contém:

6^6^6
quit

Alterando apenas isso modificou o tempo de uso de echo :

bc  0.33s user 0.00s system 80% cpu 0.414 total

Para usar o arquivo:

bc -q some  0.33s user 0.00s system 86% cpu 0.385 total

(você terá que usar o método de terdon para notar diferenças maiores, mas pelo menos sabemos que eles são)

Agora, do ponto de vista do Python, o Python precisa ler a partir do disco, compilar e executar cada vez que o arquivo, mais carregando módulos como pontos Andrew , o que torna o tempo de execução mais lento. Se você compilar o código de byte do script python, você perceberá que leva 50% menos tempo total para executar o código:

python some.py > /dev/null  0.25s user 0.01s system 63% cpu 0.413 total

compilado:

./some.pyc  0.22s user 0.00s system 77% cpu 0.282 total

Como você pode ver, há vários fatores que podem afetar a execução de tempo entre diferentes ferramentas.

    
por 21.02.2014 / 19:24
3

Eu tive o benefício de ler as outras respostas. Para começar, pessoas como eu devem saber a razão pela qual estamos lidando com um número tão grande aqui é que ambos Python e bc do associação-direita expansão de exponenciação, o que significa que isso não é 6^36 estamos avaliando, mas sim 6^46656 , que é consideravelmente maior. 1

Usando variações nos comandos a seguir, podemos extrair uma média para um elemento específico da saída da palavra time reservada e do comando:

for i in {1..1000}; do (time echo 6^6^6 | bc > /dev/null) 2>&1; done | grep 'rea' | sed -e s/.*m// | awk '{sum += $1} END {print sum / NR}'

for i in {1..1000}; do (/usr/bin/time -v sh -c 'echo 6^6^6 | bc > /dev/null') 2>&1; done | grep 'Use' | sed -e s/.*:// | awk '{sum += $1} END {print sum / NR}'

É possível ir outra rota e remover o arquivo inteiramente da comparação. Além disso, podemos comparar o tempo da bc com algo como o comando dc , já que historicamente o primeiro é um "front-end" processador "para o último. Os seguintes comandos foram cronometrados:

echo 6^6^6 | bc
echo 6 6 6 ^ ^ p | dc
echo print 6**6**6 | python2.7

Observe que o comando dc é associativo à esquerda para exponenciação. 2

Temos alguns resultados com time (bash) para 1000 iterações (em segundos):

0.229678 real bc
0.228348 user bc
0.000569 sys bc
0.23306  real dc
0.231786 user dc
0.000395 sys dc
0.07 real python
0.065907 user python
0.003141 sys python

bc e dc oferecem desempenho comparável nesse contexto.

Menos preciso 3 resultados do comando /usr/bin/time ie GNU time (a precisão da escala não é válida aqui, mas os resultados são semelhantes):

0.2224 user bc
0 sys bc
0.23 Elapsed bc
0.22998 user dc
0 sys dc
0.23 Elapsed dc
0.06008 user python
0 sys python
0.07 Elapsed python

Uma vantagem de /usr/bin/time é que ela oferece a opção -v , que produz muito mais informações que podem ser úteis eventualmente.

Também é possível avaliar isso internamente , por assim dizer, com o timeit Módulo Python:

python2.7 -m timeit -n 1000 -r 1 'print 6**6**6' | grep 'loops'
1000 loops, best of 1: 55.4 msec per loop

Isso é um pouco mais rápido do que vimos antes. Vamos tentar o próprio intérprete:

>>> import timeit
>>> import sys
>>> import os
>>> T = timeit.Timer("print 6**6**6")
>>> n = int(1000)
>>> f = open(os.devnull, 'w')
>>> sys.stdout = f
>>> t = t.timeit(n)
>>> sys.stdout = sys.__stdout__
>>> print t/n
0.0553743481636

Esse é o mais rápido que já vi.

Se avaliarmos uma exponenciação menor como 6^6 , então o comando time renderá resultados surpreendentes - usando os mesmos comandos for loop que usamos agora temos:

0.001001 bc real
0.000304 user
0.000554 sys
0.014    python real i.e. 10x more than bc??
0.010432 user
0.002606 sys

Então com um inteiro menor bc é de repente muito mais rápido ?? Da reinicialização do sistema à segunda execução não faz diferença. No entanto, ao mesmo tempo, se usarmos timeit para Python, obteremos:

python2.7 -m timeit -n 100000 -r 1 'print 6**6' | grep loops  
100000 loops, best of 1: 0.468 usec per loop

Isso é microssegundos , não milissegundos, portanto, isso não corresponde aos resultados muito mais lentos usando o loop for . Talvez outras ferramentas sejam necessárias para testar isso ainda mais e, como outros já explicaram, há mais do que o que se vê aqui. Parece que o Python foi mais rápido no cenário da pergunta, mas não está claro se as conclusões podem ser tiradas além disso ...

1. Escusado será dizer que está além do escopo de algo como o eco     expansão aritmética, ou seja, echo $((6**6**6)) - bash também acontece     para ser associativo à direita para isso, por exemplo, 6^6^6 = 6^(6^6) .

2. Compare com isto: 6 6 ^ 6 ^ p .

3. É possível que o comando de tempo GNU forneça mais informações quando executado no BSD UNIX (documento de informação de tempo GNU): A maioria das informações mostradas por 'time' é derivada da chamada de sistema 'wait3'. Os números são tão bons quanto os retornados por 'wait3'. Muitos sistemas não medem todos os recursos que o 'tempo' pode reportar; esses recursos são relatados como zero. Os sistemas que medem a maioria ou todos os recursos são baseados em 4.2 ou 4.3BSD. Versões posteriores do BSD usam códigos de gerenciamento de memória diferentes que medem menos recursos. - Em sistemas que não têm uma chamada 'wait3' que retorna informações de status, a chamada de sistema 'times' é usada no lugar. Ele fornece muito menos informações do que 'wait3', portanto, nesses sistemas, o 'tempo' relata a maioria dos recursos como zero.

    
por 23.02.2014 / 10:20