python - number - flotante numpy: 10 veces más lento que el incorporado en operaciones aritméticas?
python round float to int (8)
La respuesta es bastante simple: la asignación de memoria puede ser parte de ella, pero el mayor problema es que las operaciones aritméticas para los escalares numpy se realizan usando "ufuncs", que deben ser rápidos para varios cientos de valores, no solo 1. Hay algunos gastos generales al elegir la función correcta para llamar y configurar los bucles. Overhead que no es necesario para escalares.
Era más fácil simplemente hacer que los escalares se convirtieran en matrices de 0-d y luego pasar al nufunc ufunc correspondiente y luego escribir métodos de cálculo separados para cada uno de los muchos tipos de escalas diferentes que admite NumPy.
La intención era que las versiones optimizadas de la matemática escalar se agreguen a los objetos tipo en C. Esto todavía podría suceder, pero nunca ha sucedido porque nadie ha sido lo suficientemente motivado para hacerlo. Posiblemente porque la solución alternativa es convertir los escalares numpy en escalares de Python que sí tienen una aritmética optimizada.
Estoy obteniendo tiempos muy extraños para el siguiente código:
import numpy as np
s = 0
for i in range(10000000):
s += np.float64(1) # replace with np.float32 and built-in float
- flotador incorporado: 4.9 s
- float64: 10.5 s
- float32: 45.0 s
¿Por qué float64
es dos veces más lento que float
? ¿Y por qué es float32
5 veces más lento que float64?
¿Hay alguna forma de evitar la penalización de usar np.float64
y tener funciones numpy
devolver el float
incorporado en lugar de float64
?
Descubrí que usar numpy.float64
es mucho más lento que el float de Python, y numpy.float32
es incluso más lento (aunque estoy en una máquina de 32 bits).
numpy.float32
en mi máquina de 32 bits. Por lo tanto, cada vez que uso varias funciones numpy como numpy.random.uniform
, convierto el resultado en float32
(de modo que las operaciones posteriores se realizarían con una precisión de 32 bits).
¿Hay alguna manera de establecer una sola variable en algún lugar del programa o en la línea de comandos, y hacer que todas las funciones float32
devuelvan float32
lugar de float64
?
EDIT # 1:
numpy.float64 es 10 veces más lento que flotar en cálculos aritméticos. Es tan malo que incluso convertir para flotar y volver antes de los cálculos hace que el programa se ejecute 3 veces más rápido. ¿Por qué? ¿Hay algo que pueda hacer para solucionarlo?
Quiero enfatizar que mis tiempos no se deben a ninguno de los siguientes:
- la función llama
- la conversión entre numpy y python flotan
- la creación de objetos
Actualicé mi código para que quede más claro dónde radica el problema. Con el nuevo código, parece que veo un rendimiento de rendimiento de diez veces al usar tipos de datos numpy:
from datetime import datetime
import numpy as np
START_TIME = datetime.now()
# one of the following lines is uncommented before execution
#s = np.float64(1)
#s = np.float32(1)
#s = 1.0
for i in range(10000000):
s = (s + 8) * s % 2399232
print(s)
print(''Runtime:'', datetime.now() - START_TIME)
Los tiempos son:
- float64: 34.56s
- float32: 35.11s
- flotador: 3.53s
Solo por el placer de hacerlo, también probé:
desde la fecha de importación import datetime importa numpy como np
START_TIME = datetime.now()
s = np.float64(1)
for i in range(10000000):
s = float(s)
s = (s + 8) * s % 2399232
s = np.float64(s)
print(s)
print(''Runtime:'', datetime.now() - START_TIME)
El tiempo de ejecución es de 13.28 s; en realidad es 3 veces más rápido convertir float64
en float
y retroceder que usarlo tal como está. Aún así, la conversión cobra su peaje, por lo que en general es más de 3 veces más lenta en comparación con el float
pura pitón.
Mi máquina es:
- Intel Core 2 Duo T9300 (2.5 GHz)
- WinXP Professional (32 bits)
- ActiveState Python 3.1.3.5
- Numpy 1.5.1
EDIT # 2:
Gracias por las respuestas, me ayudan a entender cómo lidiar con este problema.
Pero todavía me gustaría saber la razón exacta (quizás basada en el código fuente) por qué el código siguiente funciona 10 veces más lento con float64
que con float
.
EDIT # 3:
Vuelvo a ejecutar el código en Windows 7 x64 (Intel Core i7 930 @ 3.8GHz).
Nuevamente, el código es:
from datetime import datetime
import numpy as np
START_TIME = datetime.now()
# one of the following lines is uncommented before execution
#s = np.float64(1)
#s = np.float32(1)
#s = 1.0
for i in range(10000000):
s = (s + 8) * s % 2399232
print(s)
print(''Runtime:'', datetime.now() - START_TIME)
Los tiempos son:
- float64: 16.1s
- float32: 16.1s
- flotar: 3.2s
Ahora ambos flotantes np
(64 o 32) son 5 veces más lentos que el float
incorporado. Aún así, una diferencia significativa. Estoy tratando de descubrir de dónde viene.
FIN DE EDICIONES
Operar con objetos de Python en un bucle pesado como ese, si son float
, np.float32
, siempre es lento. NumPy es rápido para operaciones en vectores y matrices, porque todas las operaciones se realizan en grandes porciones de datos por partes de la biblioteca escritas en C, y no por el intérprete de Python. El código ejecutado en el intérprete y / o el uso de objetos de Python siempre es lento, y el uso de tipos no nativos lo hace aún más lento. Eso es de esperar.
Si su aplicación es lenta y necesita optimizarla, intente convertir su código en una solución vectorial que use NumPy directamente, y sea rápida, o podría usar herramientas como Cython para crear una implementación rápida del ciclo en C .
Puedo confirmar los resultados también. Traté de ver cómo se vería usando todos los tipos numpy, y la diferencia persiste. Entonces, mis pruebas fueron:
def testStandard(length=100000):
s = 1.0
addend = 8.0
modulo = 2399232.0
startTime = datetime.now()
for i in xrange(length):
s = (s + addend) * s % modulo
return datetime.now() - startTime
def testNumpy(length=100000):
s = np.float64(1.0)
addend = np.float64(8.0)
modulo = np.float64(2399232.0)
startTime = datetime.now()
for i in xrange(length):
s = (s + addend) * s % modulo
return datetime.now() - startTime
Entonces, en este punto, los tipos numpy están todos interactuando entre sí, pero la diferencia de 10x persiste (2 segundos frente a 0.2 segundos).
Si tuviera que adivinar, diría que hay dos posibles razones por las cuales los tipos de flotación predeterminados son mucho más rápidos. La primera posibilidad es que python realice optimizaciones significativas bajo el capó para tratar ciertas operaciones numéricas o bucles en general (por ejemplo, desenrollar bucles). La segunda posibilidad es que los tipos numpy implican una capa adicional de abstracción (es decir, tener que leer desde una dirección). Para ver los efectos de cada uno, hice algunos controles adicionales.
Una diferencia podría ser el resultado de que python tenga que tomar pasos adicionales para resolver los tipos de float64. A diferencia de los lenguajes compilados que generan tablas eficientes, python 2.6 (y tal vez 3) tiene un costo significativo para resolver cosas que generalmente se consideran gratuitas. Incluso una resolución simple de Xa tiene que resolver el operador de punto CADA vez que se invoca. (Por eso, si tiene un bucle que llama a instancia.función (), es mejor que tenga una variable "función = instancia.función" declarada fuera del bucle).
Desde mi entendimiento, cuando usas operadores estándar de Python, estos son bastante similares a usar los de "operador de importación". Si sustituye add, mul y mod in para su +, * y%, verá un impacto de rendimiento estático de aproximadamente 0,5 segundos frente a los operadores estándar (en ambos casos). Esto significa que al envolver a los operadores, las operaciones estándar de flotación python se vuelven 3 veces más lentas. Si lo hace más, usar operator.add y esas variantes agrega aproximadamente 0,7 segundos (más de 1m de prueba, comenzando con 2 segundos y 0.2 segundos respectivamente). Eso es rayano en la lentitud 5x. Entonces, básicamente, si cada uno de estos problemas ocurre dos veces, básicamente estás en el punto 10 veces más lento.
Asumamos que somos el intérprete de Python por un momento. Caso 1, hacemos una operación en tipos nativos, digamos a + b. Debajo del capó, podemos verificar los tipos de ayb y enviar nuestra adición al código optimizado de Python. Caso 2, tenemos una operación de otros dos tipos (también a + b). Debajo del capó, comprobamos si son tipos nativos (no lo son). Pasamos al caso ''else''. El caso else nos envía a algo como a. agregar (b). a. add puede hacer un despacho al código optimizado de numpy. Entonces, en este momento, hemos tenido una sobrecarga adicional de una sucursal adicional, una ''''. obtener propiedad de slots, y una llamada a función. Y solo entramos en la operación de adición. Luego tenemos que usar el resultado para crear un nuevo float64 (o alterar un float64 existente). Mientras tanto, el código nativo de python probablemente haga trampa tratando sus tipos especialmente para evitar este tipo de sobrecarga.
Con base en el examen anterior sobre el costo de las llamadas a la función python y la sobrecarga del alcance, sería muy fácil para Numpy incurrir en una penalización de 9x solo para ir y volver de sus funciones matemáticas c. Puedo imaginar que este proceso demore mucho más que una simple llamada de operación matemática. Para cada operación, la biblioteca numpy tendrá que recorrer capas de python para llegar a su implementación de C.
Entonces, en mi opinión, la razón para esto probablemente se capta en este efecto:
length = 10000000
class A():
X = 10
startTime = datetime.now()
for i in xrange(length):
x = A.X
print "Long Way", datetime.now() - startTime
startTime = datetime.now()
y = A.X
for i in xrange(length):
x = y
print "Short Way", datetime.now() - startTime
Este caso simple muestra una diferencia de 0,2 segundos frente a 0,14 segundos (a corto plazo, más rápido, obviamente). Creo que lo que estás viendo es principalmente un montón de esos problemas que se suman.
Para evitar esto, puedo pensar en algunas posibles soluciones que hacen eco principalmente de lo que se ha dicho. La primera solución es tratar de mantener sus evaluaciones dentro de NumPy tanto como sea posible, como dijo Selinap. Una gran cantidad de pérdidas probablemente se deba a la interconexión. Buscaría formas de enviar tu trabajo a numpy o alguna otra biblioteca numérica optimizada en C (se ha mencionado gmpy). El objetivo debe ser introducir tanto en C al mismo tiempo como sea posible, y luego recuperar los resultados. Desea realizar grandes trabajos, no muchos trabajos pequeños.
La segunda solución, por supuesto, sería hacer más de tus operaciones intermedias y pequeñas en Python si puedes. Claramente, usar los objetos nativos será más rápido. Serán las primeras opciones en todas las declaraciones de sucursales y siempre tendrán la ruta más corta hacia el código C. A menos que tenga una necesidad específica de cálculo de precisión fijo u otros problemas con los operadores predeterminados, no veo por qué uno no usaría las funciones de pitón para muchas cosas.
Realmente extraño ... confirmo los resultados en Ubuntu 11.04 32bit, python 2.7.1, numpy 1.5.1 (paquetes oficiales):
import numpy as np
def testfloat():
s = 0
for i in range(10000000):
s+= float(1)
def testfloat32():
s = 0
for i in range(10000000):
s+= np.float32(1)
def testfloat64():
s = 0
for i in range(10000000):
s+= np.float64(1)
%time testfloat()
CPU times: user 4.66 s, sys: 0.06 s, total: 4.73 s
Wall time: 4.74 s
%time testfloat64()
CPU times: user 11.43 s, sys: 0.07 s, total: 11.50 s
Wall time: 11.57 s
%time testfloat32()
CPU times: user 47.99 s, sys: 0.09 s, total: 48.08 s
Wall time: 48.23 s
No veo por qué float32 debería ser 5 veces más lento que float64.
Si buscas una aritmética escalar rápida, deberías buscar bibliotecas como gmpy
lugar de numpy
(como otros han notado, esta última se optimiza más para operaciones vectoriales que escalares).
Tal vez, es por eso que debe usar Numpy directamente en lugar de usar loops.
s1 = np.ones(10000000, dtype=np.float)
s2 = np.ones(10000000, dtype=np.float32)
s3 = np.ones(10000000, dtype=np.float64)
np.sum(s1) <-- 17.3 ms
np.sum(s2) <-- 15.8 ms
np.sum(s3) <-- 17.3 ms
Los flotantes CPython se asignan en fragmentos
El problema clave al comparar las asignaciones escalares numpy con el tipo float
es que CPython siempre asigna la memoria para objetos float
e int
en bloques de tamaño N.
Internamente, CPython mantiene una lista enlazada de bloques lo suficientemente grandes como para contener N objetos float
. Cuando llamas a float(1)
CPython comprueba si hay espacio disponible en el bloque actual; si no, asigna un nuevo bloque. Una vez que tiene espacio en el bloque actual, simplemente inicializa ese espacio y le devuelve un puntero.
En mi máquina, cada bloque puede contener 41 objetos float
, por lo que hay un poco de sobrecarga para la primera llamada float(1)
pero los siguientes 40 se ejecutan mucho más rápido ya que la memoria está asignada y lista.
Slow numpy.float32 vs. numpy.float64
Parece que Numpy tiene 2 rutas que puede tomar al crear un tipo escalar: rápido y lento. Esto depende de si el tipo escalar tiene una clase base de Python a la que puede diferir para la conversión de argumentos.
Por alguna razón, numpy.float32
está codificado para tomar la ruta más lenta (definida por la macro _WORK0
) , mientras que numpy.float64
tiene la oportunidad de tomar la ruta más rápida (definida por la macro _WORK1
) . Tenga en cuenta que scalartypes.c.src
es una plantilla que genera scalartypes.c
en tiempo de compilación.
Puedes visualizar esto en Cachegrind. He incluido capturas de pantalla que muestran cuántas llamadas más se hacen para construir un float32
contra float64
:
float64
toma el camino rápido
float32
toma el camino lento
Actualizado : el tipo que toma la ruta lenta / rápida puede depender de si el sistema operativo es de 32 bits frente a 64 bits. En mi sistema de prueba, Ubuntu Lucid de 64 bits, el tipo float64
es 10 veces más rápido que float32
.
Resumen
Si una expresión aritmética contiene numpy
y números incorporados, la aritmética de Python funciona más lentamente. Evitar esta conversión elimina casi toda la degradación del rendimiento que informé.
Detalles
Tenga en cuenta que en mi código original:
s = np.float64(1)
for i in range(10000000):
s = (s + 8) * s % 2399232
los tipos float
y numpy.float64
se mezclan en una expresión. ¿Quizás Python tuvo que convertirlos a todos en un solo tipo?
s = np.float64(1)
for i in range(10000000):
s = (s + np.float64(8)) * s % np.float64(2399232)
Si el tiempo de ejecución no cambia (en lugar de aumentar), sugeriría que eso es lo que Python estaba haciendo bajo el capó, explicando el arrastre de rendimiento.
¡En realidad, el tiempo de ejecución cayó 1.5 veces! ¿Como es posible? ¿No es lo peor que Python podría tener que hacer estas dos conversiones?
Realmente no lo sé Quizás Python tuvo que verificar dinámicamente lo que se debe convertir en qué, lo que lleva tiempo, y que se le diga qué conversiones precisas realizar para hacerlo más rápido. Tal vez, se utiliza un mecanismo completamente diferente para la aritmética (que no implica conversiones en absoluto), y resulta súper lento en tipos no coincidentes. Leer código fuente numpy
podría ayudar, pero está más allá de mi habilidad.
De todos modos, ahora podemos acelerar las cosas más al mover las conversiones fuera del ciclo:
q = np.float64(8)
r = np.float64(2399232)
for i in range(10000000):
s = (s + q) * s % r
Como se esperaba, el tiempo de ejecución se reduce sustancialmente: en otras 2.3 veces.
Para ser justos, ahora necesitamos cambiar ligeramente la versión float
, al mover las constantes literales fuera del ciclo. Esto resulta en una pequeña desaceleración (10%).
Contabilizando todos estos cambios, la versión np.float64
del código ahora es solo un 30% más lenta que la versión float
equivalente; el ridículo golpe de rendimiento de 5 veces se ha ido en gran parte.
¿Por qué todavía vemos el 30% de retraso? numpy.float64
números numpy.float64
ocupan la misma cantidad de espacio que float
, por lo que esa no será la razón. Tal vez la resolución de los operadores aritméticos demore más en los tipos definidos por el usuario. Ciertamente no es una gran preocupación.