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

29

Estou avaliando a expressão 6^6^6usando pythone bcseparadamente.

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

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

E então, eu executei o comando time echo 6^6^6 | bcque 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 gasto por python e bc foi de 11 ms e 5 ms, respectivamente. O comando bc superou o python em nível de tempo do sistema, mas quando se trata de usuário e em tempo real, o python foi quase quatro vezes mais rápido que o bc . O que poderia ter acontecido lá. Eu não dei nenhuma prioridade aos processos como tais. Eu estou tentando entender essa situação.

ganessh
fonte
Então você quer dizer que o componente sys fornece apenas o tempo necessário para carregar e o tempo de execução será fornecido no componente de usuário da saída?
21814 Ganessh
Não tenho muita certeza, por isso postei um comentário. É apenas um palpite.
terdon
7
echo | bcenvolve o lançamento de um subshell por causa do canal - é daí que provavelmente parte do seu tempo extra de usuário. Para fazer deste um teste equitativo, o script python deve ler em stdin para que você possa time echo 6**6**6 | whatever.py.
GOLDILOCKS
1
Prefiro colocar a linha de comando be em um script e cronometrar a execução disso. Ou use echo 6^6^6 | time bc.
Daniel kullmann
1
Nota lateral: em python, a 6**6**6expressão é realmente calculada em tempo de compilação . No entanto, como você está iniciando o arquivo diretamente em vez de importá-lo de um módulo, isso não deve importar. Para ver a diferença colocada 10**12345678em um a.pyarquivo e tentar importá-lo do intérprete interativo. Depois feche o intérprete, reinicie-o e importe anovamente. A primeira vez que deve ter uma quantidade considerável de tempo (porque pitão está a compilar o módulo), enquanto que a segunda vez que ele carrega o .pyc, que deve ser instantânea,
Bakuriu

Respostas:

25

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

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

Cada uma delas requer um número ainda maior de tentativas para abrir um arquivo Python, porque existem várias 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 as incorporadas, requer chamadas de sistema operacional / sistema, e cada "importação" parece acionar cerca de 8 mensagens de "tentativa". (Havia maneiras de reduzir isso usando zipimport, e cada caminho no seu PYTHONPATH pode exigir outra chamada.)

Isso significa que existem quase 200 chamadas de sistema stat antes que o Python inicie na minha máquina, e "time" atribui isso a "sys" em vez de "user", porque o programa do usuário aguarda o sistema para fazer as coisas.

Por comparação, e como Terdon disse, "bc" não tem um custo inicial tão alto. Observando a saída dtruss (eu tenho um Mac; "strace" para um sistema operacional baseado em Linux), vejo que o bc não faz nenhuma chamada de sistema aberta () ou stat (), exceto por carregar algumas compartilhadas bibliotecas são o começo, o que obviamente o Python também. Além disso, o Python tem mais arquivos para ler, antes de estar pronto para processar qualquer coisa.

A espera do disco está lenta.

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; portanto, o custo de inicialização é de 1/2 do tempo total e o cálculo + conversão para decimal é a outra metade. Enquanto:

time echo 1 | bc

leva 0,005s e "6 ^ 6 ^ 6" leva 0,184s, de modo que a exponenciação de bc é 4x mais lenta que a de Python, embora seja 7x mais rápido para começar.

Andrew Dalke
fonte
4
Você meio que enterrou a liderança lá. Você pode mover o bit final para o topo.
Riking 22/02
Apenas por interesse na minha máquina: time python -c 'pass' 0m0.025s, time python -c 'print 6 6 6' 0m0.087s mas time python -c 'x = 6 6 6' 0m0.028s Portanto, a maioria dos o tempo está emitindo o número grande.
Steve Barnes
Sim, a conversão para a base 10 leva tempo quadrático no número de dígitos. Como um caso extremo, tente imprimir um dos números primos Mersenne maiores. É muito rápido para calcular, mas leva muito tempo para imprimir na base 10. #
6267 Andrew Dalke
11

Encontrei uma boa resposta para explicar os diferentes campos:

  • Real é a hora do relógio de parede - hora do início ao fim da chamada. Todo esse tempo decorrido, incluindo intervalos de tempo usados ​​por outros processos e o tempo que o processo passa bloqueado (por exemplo, se estiver aguardando a conclusão da E / S).

  • Usuário é a quantidade de tempo de CPU gasto no código do modo de usuário (fora do kernel) dentro do processo. Este é apenas o tempo real da CPU usado na execução do processo. Outros processos e tempo que o processo gasta bloqueado não contam para esse valor.

  • Sys é a quantidade de tempo de CPU gasto no kernel dentro do processo. Isso significa executar o tempo de CPU gasto em chamadas do sistema no kernel, em oposição ao código da biblioteca, que ainda está sendo executado no espaço do usuário. Como 'usuário', esse é apenas o tempo da CPU usado pelo processo. Veja abaixo uma breve descrição do modo do kernel (também conhecido como modo 'supervisor') e o mecanismo de chamada do sistema.

Portanto, no seu exemplo específico, a versão python é mais rápida em termos do tempo real que leva para ser concluído. No entanto, a abordagem python gasta mais tempo no espaço do kernel, fazendo chamadas para funções do kernel. O bccomando gasta essencialmente pouco tempo no espaço do kernel e todo o seu tempo é gasto no espaço do usuário, presumivelmente executando o bccódigo interno .

Isso não faz diferença para você, a única informação com a qual você realmente se importa é realqual é o tempo real decorrido entre iniciar o comando e obter sua saída.

Você também deve estar ciente de que essas pequenas 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
terdon
fonte
10

Vou explicar de outra perspectiva.

Para ser justo, bctem 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 + lendo um arquivo. Portanto, seu teste pode ser tendencioso bc. Para testá-lo, você deve usar bc -q filewhere filecontém:

6^6^6
quit

Mudando exatamente isso modificou o tempo de uso 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ê precisará usar o método de terdon para perceber diferenças maiores, mas pelo menos sabemos que são)

Agora, da perspectiva do python, o python precisa ler do disco, compilar e executar cada vez que o arquivo, além de carregar módulos como aponta Andrew , o que torna o tempo de execução mais lento. Se você compilar o código de bytes do script python, notará 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, existem vários fatores que podem afetar a execução do tempo entre diferentes ferramentas.

Braiam
fonte
3

Eu tive o benefício de ler as outras respostas. Para começar pessoas como eu deveria saber a razão pela qual estamos lidando com um grande número inteiro tal aqui é que tanto Pythone bcfazer direito associativo expansão exponenciação, o que significa que este não é 6^36que estamos avaliando, mas sim 6^46656que é consideravelmente maior. 1

Usando variações nos seguintes comandos, podemos extrair uma média para um elemento específico da saída da timepalavra e do comando reservados:

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 seguir outra rota e remover o arquivo inteiramente da comparação. Além disso, podemos comparar o tempo do bc com algo como o dccomando, pois historicamente o primeiro é um "processador de front-end" para o segundo. 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 dccomando é 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

bce dcoferecer desempenho comparável nesse contexto.

Resultados menos precisos 3, por exemplo, do comando /usr/bin/timeGNU 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 /usr/bin/timedisso é que ele oferece a -vopção que gera muito mais informações que podem ser úteis eventualmente.

Também é possível avaliar isso internamente, por assim dizer, com o timeitmó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 o 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

É o mais rápido que eu já vi.


Se avaliarmos uma exponenciação menor 6^6, o comando time produzirá resultados surpreendentes - usando os mesmos forcomandos de 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 número inteiro menor, bcde 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 timeitpara Python, obtemos:

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 forloop. Talvez outras ferramentas sejam necessárias para testar isso ainda mais e, como outros explicaram, há mais do que aparenta aqui. Parece que o Python foi mais rápido no cenário da pergunta, mas não está claro se é possível tirar conclusões além disso ...


1. Escusado será dizer que está além do escopo de algo como a expansão aritmética do eco, isto é echo $((6**6**6))- bashtambém é associativa correta para esse exemplo 6^6^6 = 6^(6^6).

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

3. É possível que o comando GNU time forneça mais informações quando executado no BSD UNIX (documento de informações da hora 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 sobre os quais o 'tempo' pode ser reportado; 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ódigo de gerenciamento de memória diferente que mede menos recursos. - Nos sistemas que não possuem uma chamada 'wait3' que retorna informações de status, a chamada do sistema 'times' é usada. Ele fornece muito menos informações do que 'wait3'; portanto, o 'tempo' desses sistemas informa a maioria dos recursos como zero.

Comunidade
fonte