Consejos tipo Python 3 para optimizaciones de rendimiento

PEP 484 dice “El uso de sugerencias de tipo para las optimizaciones de rendimiento se deja como un ejercicio para el lector”. Esto me sugiere que, al igual que Common Lisp, las declaraciones de tipo se pueden usar para dejar de lado el tipo de despacho dentro de las funciones de rendimiento intensivo cuando juro que sé lo que hago. Para probar esto por mí mismo, preparé un pequeño punto de referencia para calcular pi utilizando una p-series. Primero lo hago de forma ingenua, luego trato de ser inteligente y explotar las sugerencias de tipo para el rendimiento:

import math import time def baselpi0(n): baselsum = 0; for i in range(1,n): baselsum += 1.0 / (i * i) return math.sqrt(6.0 * baselsum) def baselpi1(n : int) -> float: n = float(n) baselsum = 0.0 i = 1.0 while i < n: baselsum += 1.0 / (i * i) i += 1.0 return math.sqrt(6.0 * baselsum) start = time.time() print(baselpi0(1000000000)) end = time.time() print(end - start) start = time.time() print(baselpi1(1000000000)) end = time.time() print(end - start) 

La analogía de Common Lisp que estoy tratando de emular es:

 (defun baselpi0 (n) (let ((baselsum 0.0d0)) (loop for i from 1 to n do (setf baselsum (+ baselsum (/ 1.0 (* ii))))) (sqrt (* 6 baselsum)))) (defun baselpi1 (n) (let ((baselsum 0.0d0) (n (coerce n 'double-float))) (declare (type double-float baselsum n) (optimize (speed 3) (safety 0) (debug 0))) (loop for i from 1.0d0 to n do (setf baselsum (+ baselsum (/ 1.0d0 (* ii))))) (sqrt (* 6.0d0 baselsum)))) (time (princ (baselpi0 1000000000))) (time (princ (baselpi1 1000000000))) (exit) 

En mi máquina, la versión lisp ejecutada con sbcl toma 22 segundos para la versión lenta y 4 segundos para la versión de tipo insinuado, igual que C. CPython toma 162 segundos para la versión ingenua y 141 para la versión de tipo insinuado. Pypy ejecuta la versión no insinuada en menos de 5 segundos, pero el soporte de la biblioteca no es lo suficientemente bueno para mi proyecto.

¿Hay alguna manera de mejorar mi versión de tipo insinuado para obtener un rendimiento más cercano a lisp o Pypy?

La diferencia de velocidad no se debe a insinuación de tipo. Actualmente, Python , y en el futuro previsible, simplemente descarta cualquier sugerencia que ofrezca y continúa ejecutándose de forma dinámica como siempre lo hace .

Esto se debe al hecho de que en un caso se usa aritmética flotante en todo el código (lo que se traduce en una ejecución más rápida), mientras que en el otro no.

Caso en cuestión: Cambie baselpi1 a lo siguiente:

 def baselpi1(n : int) -> float: n = float(n) baselsum = 0 i = 1 while i < n: baselsum += 1.0 / (i * i) i += 1 return math.sqrt(6.0 * baselsum) 

Y mira ahora los tiempos de ejecución:

 3.141591698659554 0.2511475086212158 3.141591698659554 0.4525010585784912 

Sí, es mucho más lento.

Si necesita realizar grandes cantidades de cómputo numérico, entonces numpy ofrece una buena opción en general. Numpy funciona con tipos de datos de nivel inferior (como los enteros de ancho fijo – python’s no tiene límites). Esto le proporciona el tipo de sugerencia de tipo en el que está interesado. Dado que numpy está diseñado para trabajar con grandes cantidades de datos en matrices con tipos conocidos, puede realizar la misma operación de manera eficiente en una matriz completa. Esto también permite que numpy funcione bien con las CPU que las instrucciones SIMD (no conozco una CPU moderna sin SIMD).

Normalmente reescribiría tu función como tal:

 import math import numpy def baselpi_numpy(n): i = numpy.arange(1, n) # array of 1..n baselsum = (1 / (i * i)).sum() return math.sqrt(6 * baselsum) 

Sin embargo, para grandes n no tendrá suficiente memoria. Tendrá que agregar un poco de código adicional para realizar la operación por lotes por usted. Es decir:

 def baselpi_numpy(n, batch_size=1 << 16): basel_sum = 0 i = 1 for i in range(1, n, batch_size): j = min(n, i + batch_size) basel_sum += baselsum_numpy(i, j) return math.sqrt(6 * basel_sum) def baselsum_numpy(start, end): # equivalent -> sum(1 / (i * i) for i in range(start, end)) i = numpy.arange(start, end, dtype=float) # this line and next are memory optimisations which double speed # equivalent to i = 1 / (i * i) i *= ii = numpy.divide(1, i, out=i) basel_sum = i.sum() return basel_sum 

Recupero el resultado en 5.2 segundos en mi computadora portátil. Aunque no he probado el valor de n que usas, para n más bajo, la versión numpy es 20 veces más rápida.