python vs bc al evaluar 6 ^ 6 ^ 6

29

Estoy evaluando la expresión 6^6^6usando pythony por bcseparado.

El contenido del archivo python es print 6**6**6. Cuando ejecuto time python test.py, obtengo la salida como

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

Y luego, ejecuté el comando time echo 6^6^6 | bcque me dio el siguiente resultado

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

A partir de estos resultados, está claro que el tiempo del sistema tomado por python y bc fue de 11 ms y 5 ms respectivamente. El comando bc superó a Python en el nivel de tiempo del sistema, pero en lo que respecta al usuario y a Python en tiempo real fue casi 4 veces más rápido que bc . Lo que pudo haber ido allí. No he dado ninguna prioridad a los procesos como tal. Estoy tratando de entender esta situación.

ganessh
fuente
Entonces, ¿quiere decir que el componente sys solo da el tiempo que lleva cargar y el tiempo de ejecución se dará en el componente de usuario de la salida?
ganessh
Realmente no estoy seguro, por eso publiqué un comentario. Es solo una suposición.
terdon
77
echo | bcimplica el lanzamiento de una subshell debido a la canalización, de ahí es probable que provenga parte de su tiempo de usuario adicional. Para hacer de esto una prueba equitativa, el script de Python debe leer de stdin para que pueda time echo 6**6**6 | whatever.py.
Ricitos
1
Prefiero poner la línea de comando be en un script y cronometrar la ejecución de eso. O usar echo 6^6^6 | time bc.
daniel kullmann
1
Nota al margen: en python la 6**6**6expresión se calcula en tiempo de compilación . Sin embargo, dado que está iniciando el archivo directamente en lugar de importarlo desde un módulo, esto no debería importar. Para ver la diferencia puesta 10**12345678en un a.pyarchivo e intentar importarlo desde el intérprete interactivo. Luego cierre el intérprete, reinícielo e importe anuevamente. La primera vez debería llevar una cantidad de tiempo notable (porque Python está compilando el módulo), mientras que la segunda vez carga el .pyc, que debería ser instantáneo,
Bakuriu

Respuestas:

25

Python importa una gran cantidad de archivos al inicio:

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

Cada uno de estos requiere un número aún mayor de intentos de abrir un archivo Python, porque hay muchas formas de definir un 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 "intento", excepto los que están integrados, requiere un nivel de sistema operativo / llamadas al sistema, y ​​cada "importación" parece desencadenar alrededor de 8 mensajes de "intento". (Hubo formas de reducir esto usando zipimport, y cada ruta en su PYTHONPATH puede requerir otra llamada).

Esto significa que hay casi 200 llamadas al sistema de estadísticas antes de que Python se inicie en mi máquina, y "time" asigna eso a "sys" en lugar de "usuario", porque el programa de usuario está esperando que el sistema haga las cosas.

En comparación, y como dijo terdon, "bc" no tiene ese alto costo inicial. Al observar la salida de dtruss (tengo una Mac; "strace" para un sistema operativo basado en Linux), veo que bc no hace ninguna llamada al sistema open () o stat (), excepto para cargar algunas compartidas las bibliotecas son el comienzo, que por supuesto Python también lo hace. Además, Python tiene más archivos para leer, antes de que esté listo para procesar cualquier cosa.

Esperar el disco es lento.

Puede tener una idea del costo de inicio de Python haciendo:

time python -c pass

Son 0.032s en mi máquina, mientras que 'print 6 ** 6 ** 6' es 0.072s, por lo que el costo de inicio es 1 / 2rd del tiempo total y el cálculo + conversión a decimal es la otra mitad. Mientras:

time echo 1 | bc

toma 0.005s, y "6 ^ 6 ^ 6" toma 0.184s por lo que la exponenciación de bc es 4 veces más lenta que la de Python a pesar de que es 7 veces más rápido para comenzar.

Andrew Dalke
fuente
44
De alguna manera enterraste el plomo allí. Es posible que desee mover el bit final a la parte superior.
Riking
Solo por interés en mi máquina: time python -c 'pass' 0m0.025s, time python -c 'print 6 6 6' 0m0.087s pero time python -c 'x = 6 6 6' 0m0.028s Así que la mayoría de el tiempo está generando el gran número.
Steve Barnes
Sí, la conversión a la base 10 toma tiempo cuadrático en el número de dígitos. Como caso extremo, intente imprimir uno de los primos Mersenne más grandes. Es muy rápido de calcular, pero lleva mucho tiempo imprimir en la base 10.
Andrew Dalke
11

Encontré una buena respuesta en SO que explica los diferentes campos:

  • Real es la hora del reloj de pared: tiempo desde el inicio hasta el final de la llamada. Este es todo el tiempo transcurrido, incluidos los segmentos de tiempo utilizados por otros procesos y el tiempo que el proceso pasa bloqueado (por ejemplo, si está esperando que se complete la E / S).

  • Usuario es la cantidad de tiempo de CPU gastado en código de modo de usuario (fuera del núcleo) dentro del proceso. Este es solo el tiempo real de CPU utilizado en la ejecución del proceso. Otros procesos y el tiempo que el proceso pasa bloqueado no cuentan para esta cifra.

  • Sys es la cantidad de tiempo de CPU que se pasa en el núcleo dentro del proceso. Esto significa ejecutar el tiempo de CPU gastado en las llamadas del sistema dentro del núcleo, a diferencia del código de la biblioteca, que todavía se ejecuta en el espacio del usuario. Al igual que 'usuario', este es solo el tiempo de CPU utilizado por el proceso. Consulte a continuación una breve descripción del modo kernel (también conocido como modo 'supervisor') y el mecanismo de llamada del sistema.

Entonces, en su ejemplo específico, la versión de Python es más rápida en términos del tiempo real que lleva completar. Sin embargo, el enfoque de Python pasa más tiempo en el espacio del núcleo, haciendo llamadas a las funciones del núcleo. El bccomando no pasa esencialmente tiempo en el espacio del kernel y todo su tiempo se gasta en el espacio del usuario, presumiblemente ejecutando bccódigo interno .

Esto no hace ninguna diferencia para usted, la única información que realmente le importa es realcuál es el tiempo real transcurrido entre el lanzamiento del comando y la obtención de su salida.

También debe tener en cuenta que estas pequeñas diferencias no son estables, también dependerán de la carga de su sistema y cambiarán cada vez que ejecute el 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
fuente
10

Lo explicaré desde otra perspectiva.

Para ser justos, bctiene ventaja ya que no tiene que leer nada del disco y solo necesita sus blob / binarios, mientras que python tiene que importar una serie de módulos + leer un archivo. Entonces su prueba puede estar sesgada hacia bc. Para probarlo realmente debes usar bc -q filewhere filecontiene:

6^6^6
quit

Cambiar solo eso modificó el tiempo de uso echo:

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

Para usar el archivo:

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

(deberá usar el método de terdon para notar mayores diferencias, pero al menos sabemos que lo son)

Ahora, desde la perspectiva de Python, Python necesita leer desde el disco, compilar y ejecutar cada vez que el archivo, además de cargar módulos como señala Andrew , lo que hace que el tiempo de ejecución sea más lento. Si compila el código de bytes del script de Python, notará que se necesita un 50% menos de tiempo total para ejecutar el 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 puede ver, hay varios factores que pueden afectar la ejecución del tiempo entre diferentes herramientas.

Braiam
fuente
3

He tenido el beneficio de leer las otras respuestas. Para empezar la gente como yo deben saber la razón por la que estamos tratando con un enorme número entero tal aquí es que tanto Pythony bchacer asociativo por la derecha expansión exponencial, lo que significa que este no es 6^36que estamos evaluando, sino 6^46656que es considerablemente más grande. 1

Usando variaciones en los siguientes comandos, podemos extraer un promedio para un elemento específico de la salida de la timepalabra reservada y el 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}'

Es posible ir a otra ruta y eliminar el archivo por completo de la comparación. Además, podemos comparar el tiempo de bc con algo como el dccomando, ya que históricamente el primero es un "procesador frontal" para el segundo. Los siguientes comandos fueron cronometrados:

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

Tenga en cuenta que el dccomando es asociativo a la izquierda para exponenciación. 2

Tenemos algunos resultados con time(bash) para 1000 iteraciones (en 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

bcy dcofrecer un rendimiento comparable en este contexto.

Resultados menos precisos 3,/usr/bin/time es decir, timecomando GNU (la precisión de la escala no es válida aquí pero los resultados son similares):

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

Una ventaja de esto /usr/bin/timees que ofrece la -vopción que produce mucha más información que podría ser útil eventualmente.

También es posible evaluar esto internamente, por así decirlo con el 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

Eso es un poco más rápido de lo que vimos antes. Probemos con el propio 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

Eso es lo más rápido que he visto.


Si evaluamos una exponenciación menor 6^6, entonces el comando de tiempo arroja resultados sorprendentes: usando los mismos forcomandos de bucle que usamos ahora tenemos:

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

Entonces, ¿con un número entero más pequeño bces de repente mucho más rápido? Desde el reinicio del sistema hasta la segunda ejecución no hay diferencia. Sin embargo, al mismo tiempo, si usamos timeitpara Python, obtenemos:

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

Esto es microsegundos , no milisegundos, por lo que esto no coincide con los resultados mucho más lentos utilizando el forbucle. Tal vez se requieran otras herramientas para probar esto más y, como otros han explicado, hay más de lo que parece a simple vista. Parece que Python fue más rápido en el escenario de la pregunta, pero no está claro si se pueden sacar conclusiones más allá de eso ...


1. No hace falta decir que está más allá del alcance de algo como la expansión aritmética del eco, es decir echo $((6**6**6)), bashtambién resulta ser una asociación correcta para eso 6^6^6 = 6^(6^6).

2. Comparar con esto: 6 6 ^ 6 ^ p.

3. Es posible que el comando de tiempo de GNU proporcione más información cuando se ejecuta en BSD UNIX (documento de información de tiempo de GNU): la mayor parte de la información mostrada por 'tiempo' se deriva de la llamada al sistema 'wait3'. Los números son tan buenos como los devueltos por 'wait3'. Muchos sistemas no miden todos los recursos sobre los que puede informar el "tiempo"; esos recursos se informan como cero. Los sistemas que miden la mayoría o la totalidad de los recursos se basan en 4.2 o 4.3BSD. Las versiones posteriores de BSD utilizan un código de administración de memoria diferente que mide menos recursos. - En los sistemas que no tienen una llamada 'wait3' que devuelve información de estado, en su lugar se usa la llamada del sistema 'times'. Proporciona mucha menos información que 'wait3', por lo que en esos sistemas el 'tiempo' informa que la mayoría de los recursos son cero.

Comunidad
fuente