Rendimiento de Python: bucles nativos de NumPy, "callbacks" y costes de conversión
Una prueba numérica controlada solo para CPU sigue valores idénticos a través de bucles de CPython, aritmética de NumPy, un "wrapper" de "callback" de Python y un bucle compilado de Numba.
Una prueba numérica controlada solo para CPU sigue valores idénticos a través de bucles de CPython, aritmética de NumPy, un "wrapper" de "callback" de Python y un bucle compilado de Numba. NumPy con entrada preparada tuvo una gran ventaja, mientras que la conversión de y a listas de Python redujo esa ventaja a un empate virtual. El episodio explica el límite de ejecución y cómo medir el resultado que una aplicación necesita.
Leer la edición escrita (inglés) ↗
Qué cubre este vídeo
- CPython compila el código fuente a bytecode. El trabajo repetido del intérprete/objeto y un bucle numérico nativo tipado son diferentes arreglos de ejecución.
- La aritmética numérica de NumPy puede pasar lotes tipados a operaciones compiladas; el bucle aritmético sigue existiendo y las vistas pueden tener diseños no contiguos.
- Mida la conversión y la representación de salida requerida. En esta ejecución, la conversión de listas de Python a arrays y viceversa eliminó la gran ventaja del array preparado.
- Las cargas de trabajo pequeñas pueden favorecer a Python ordinario porque los costes fijos de las llamadas a arrays importan. La comprensión de ocho valores ganó esta comparación medida.
- np.vectorize es un "wrapper" de conveniencia que llama a Python para elementos sucesivos; una interfaz de lote por sí sola no establece la ejecución numérica nativa.
- Los bucles compilados con sintaxis de Python también pueden ser rápidos. Incluya los costes de compilación/configuración "en frío" cuando importen al "caller".
- La ejecución nativa, SIMD de CPU, multihilo y "backends" de GPU son ideas separadas. Las características de CPU disponibles no rastrean las instrucciones utilizadas en una ejecución particular.
Transcripción traducida
Traducido de la narración original en inglés. El audio y los subtítulos disponibles están controlados por YouTube.
¿Por qué la misma CPU puede terminar antes?
0:00 Crees que Python necesita un nuevo lenguaje para ser rápido. La misma CPU ejecutó nuestro lote más rápido después de que el bucle se moviera. ¿A dónde fue el trabajo? ¿Cuándo desaparece la aceleración? ¿Qué atajo trae de vuelta a Python? Esto es The Daily Diff, bajo el capó. Estate atento a ese atajo. CPython es la implementación estándar de Python.
0:17 Compila tu código fuente en "bytecode", luego ejecuta esas instrucciones en un intérprete. NumPy es una biblioteca numérica cuyos "arrays" contienen valores con un tipo de datos declarado. Su aritmética incorporada puede entregar un lote completo a operaciones compiladas. Toma un millón de valores de punto flotante, eleva al cuadrado cada uno, luego añade el valor original. Cada versión obtiene valores idénticos y produce respuestas coincidentes. Los valores son lo suficientemente pequeños como para mantener las diferencias de redondeo fuera de la carrera.
0:40 En esta máquina, la comprensión tardó unos treinta y tres milisegundos. NumPy tardó menos de uno. Ambos se ejecutaron en la CPU. Eso es una evidencia útil para esta carga de trabajo, y un obituario terrible para un lenguaje de programación. Empecemos con el bucle ordinario.
¿Qué trabajo repite un bucle de Python?
0:53 Python obtiene el siguiente valor, realiza la aritmética y añade la respuesta. Luego lo hace de nuevo. La multiplicación es barata. Organizar la siguiente multiplicación, y manejar los objetos de Python a su alrededor, añade trabajo cada vez. Esta es la desensamblado real de la función probada. La iteración, multiplicación y adición aparecen como instrucciones de "bytecode". Las versiones e intérpretes de Python toman diferentes decisiones.
1:14 Estos tiempos pertenecen a la versión en pantalla, que es Python tres punto diez. Una lista de Python almacena referencias a objetos, por lo que diferentes tipos de cosas pueden compartir una lista. Nuestro trabajo aritmético utiliza números en todo momento. Paga por un arreglo general mientras solicita un servicio repetitivo. Imagina pedir el mismo café un millón de veces y rellenar un formulario nuevo para cada taza. La cafetera está bien.
1:36 El papeleo ha desarrollado su propia carrera.
¿Dónde mueve NumPy el bucle?
1:38 NumPy cambia el contrato. Este array contiene valores de punto flotante de tamaño fijo con un tipo declarado. Nuestro array es contiguo, por lo que sus valores se encuentran juntos en memoria. Las vistas pueden tener otros diseños. La operación aritmética sabe qué tipo de datos está recibiendo. La llamada pasa el lote a una operación nativa. Un bucle sigue visitando los valores dentro del código compilado. Puede reutilizar el conocimiento del tipo en todo ese trabajo.
2:00 Python sigue siendo la persona que entrega el trabajo. Itamar Turner Trauring explica la distinción en Python Speed. Las interfaces de lote, las implementaciones nativas y las instrucciones vectoriales del procesador son cosas separadas. El nombre de la función aporta muy poca evidencia.
¿Significa "rápido" GPU o muchos núcleos de CPU?
2:13 Este resultado utilizó NumPy en la CPU. Un "backend" de GPU como CuPy elige un hogar diferente para "arrays" y operaciones. Comprar una GPU deja este bucle ordinario de Python ejecutándose donde ya se ejecutaba. Las instrucciones vectoriales de la CPU pueden procesar varios valores juntos. NumPy tiene maquinaria para elegir los "kernels" soportados. Nuestro tiempo de ejecución informa de las características disponibles, pero no rastrea las instrucciones de esta expresión. Atribuir toda la ganancia medida a las instrucciones vectoriales sería exagerar la evidencia.
2:40 El carrito de compras de la GPU puede permanecer cerrado. Hay algo profundamente satisfactorio en una corrección de rendimiento que también cancela una compra. Desafortunadamente, el "benchmark" continúa.
¿Cuándo desaparece el impresionante aumento de velocidad?
2:49 Con ocho valores, la comprensión ganó. Llamar a la maquinaria de arrays costó más que el pequeño trabajo. Los arrays listos más grandes tomaron la delantera. Utilizamos ensayos repetidos y un proceso "calentado". Las compilaciones más recientes de Python pueden cambiar el "crossover". Luego cambia lo que la aplicación necesita. Comienza con una lista de Python y termina con una nueva lista de Python. NumPy debe convertir la entrada en un array y convertir la respuesta de nuevo en objetos
3:11 de Python. Esos pasos pertenecen a la factura si tu aplicación los necesita. Con un millón de valores, esa ruta completa tardó unos veintiocho milisegundos. La comprensión tardó unos veintiocho también. Nuestra dramática ventaja se convirtió en un empate virtual. La parte rápida era real. La ventaja dependía de dejar los datos en su forma numérica.
¿Qué límites pertenecen al "benchmark"?
3:28 Sigue los datos. Si la siguiente operación acepta un array, mantén el array. Si conviertes repetidamente entre listas y "arrays", mide esos cruces. Detenerse antes de un costoso traspaso da números precisos para la pregunta equivocada. Multiplicar este array crea un resultado temporal. Añadir los valores originales crea la salida. La ejecución nativa aún necesita memoria y mueve datos.
3:47 Algunas operaciones aceptan un búfer de salida, lo que reduce las asignaciones cuando tu código puede reutilizarlo de forma segura. Una línea bonita puede pedir varios búferes grandes. Python Speed también señala este problema. El "allocator" es notablemente indiferente a tu estética.
¿Puede un bucle de Python compilado ser rápido también?
4:00 También puedes mantener la sintaxis del bucle y cambiar su ejecución. Numba puede compilar funciones numéricas de Python compatibles en código nativo. Aquí está el bucle de nuestra prueba con su "decorator" de compilador. Todavía dice "for". La costosa ruta del intérprete ha cambiado. Después de la compilación, este bucle tardó menos de medio milisegundo con arrays preparados. La compilación en sí tardó aproximadamente un segundo en esta ejecución. Ese coste de configuración fue excluido de los tiempos repetidos,
4:22 por lo que un pequeño trabajo "one off" merece una comparación diferente. La propia guía de Numba se complace en mostrar bucles explícitos. Iniciar una migración de lenguaje antes de encontrar la "ruta caliente" es una forma elaborada de evitar abrir un "profiler". Primero averigua qué consume el tiempo. Las esperas de red, el análisis de objetos y la aritmética a granel requieren diferentes soluciones. Las operaciones nativas de array ayudan cuando el cálculo y los datos encajan.
4:43 Mide el resultado que tu "caller" realmente necesita. Ahora, para ese atajo. Tienes una función Python y quieres que acepte un array. Un "wrapper" llamado "vectorize" suena como el botón de rendimiento que faltaba. Su documentación tiene una frase importante esperando.
¿Qué ejecuta realmente np.vectorize?
4:56 NumPy dice que su "wrapper" "vectorize" se proporciona principalmente por conveniencia. Su implementación es esencialmente un bucle "for". El "wrapper" acepta lotes, pero sigue llamando a tu función Python para elementos sucesivos. La interfaz se acortó. El límite de ejecución se mantuvo ocupado. Observa cómo los mismos valores regresan a través del "callback" de Python. Con "arrays" listos, esta versión tardó unos ochenta y siete milisegundos. Fue más lento que nuestra comprensión.
5:18 Un "wrapper" de "broadcasting" útil puede preservar el trabajo exacto por elemento que estabas intentando eliminar. Comprueba la implementación detrás de una interfaz de lote, mantén los datos compatibles en su forma útil e incluye la conversión en la comparación. Los bucles nativos, de "callback" y compilados pueden parecer similares desde la perspectiva del "caller". El nombre "vectorize" hizo una promesa que tu cronómetro nunca firmó. Mantenlo cuando la conveniencia es lo que necesitas. Para el rendimiento, comprueba el bucle que se ejecuta.
¿Qué deberías cambiar en tu código?
5:41 Mi veredicto es SHIP IT para "batching" una "ruta caliente" numérica medida. Mantén los bucles ordinarios cuando ya funcionen bien. Comprueba dónde ocurre el trabajo y qué necesita el siguiente "caller". Suscríbete, pulsa la campana y dime en los comentarios si lo habrías sellado de manera diferente. Y esa es la diferencia de hoy. Soy Niko de Axrisi. Fusiona con responsabilidad.
Fuentes
- Universal functions (ufunc) basics — NumPy 2.2NumPy
- The N-dimensional array — NumPy 2.2NumPy
- numpy.vectorize — convenience and performance notesNumPy
- CPU/SIMD optimizations — NumPy 2.2NumPy
- dis — Disassembler for Python bytecodePython Software Foundation
- CPython 3.10.12 list representationCPython contributors
- The limits of Python vectorization as a performance techniqueItamar Turner-Trauring / Python⇒Speed
- Performance TipsNumba
- Basics of CuPyCuPy



