+− THE DAILY DIFFdev & AI news
SHIP IT

Rendimiento de Python: bucles nativos de NumPy, devoluciones de llamada y costos de conversión

Una prueba numérica controlada solo con CPU sigue valores idénticos a través de bucles de CPython, aritmética de NumPy, un "wrapper" de devolución de llamada de Python y un bucle compilado de Numba.

Want to see our stories first on Google? Add us to your preferred sources — it helps us grow.

Una prueba numérica controlada solo con CPU sigue valores idénticos a través de bucles de CPython, aritmética de NumPy, un "wrapper" de devolución de llamada de Python y un bucle compilado de Numba. NumPy con entrada preparada tenía una gran ventaja, mientras que la conversión de y a listas de Python redujo esa ventaja a un empate. El episodio explica el límite de ejecución y cómo medir el resultado que necesita una aplicación.

Leer la edición escrita (inglés) ↗

Lo que cubre este video

  • 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 entregar lotes tipados a operaciones compiladas; el bucle aritmético aún existe 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 en arreglos y viceversa borró la gran ventaja del arreglo preparado.
  • Las cargas de trabajo pequeñas pueden favorecer a Python ordinario porque los costos fijos de las llamadas a arreglos 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 por lotes por sí sola no establece la ejecución numérica nativa.
  • Los bucles de sintaxis de Python compilados también pueden ser rápidos. Incluya los costos de compilación/configuración en frío cuando importen para el llamador.
  • 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 son 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. ¿Adónde fue el trabajo? ¿Cuándo se desvanece 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 arreglos 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 cada uno al cuadrado, luego agrega 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 tomó unos treinta y tres milisegundos. NumPy tomó menos de uno. Ambos se ejecutaron en la CPU. Eso es evidencia útil para esta carga de trabajo, y un obituario terrible para un lenguaje de programación. Comencemos con el bucle ordinario.

¿Qué trabajo repite un bucle de Python?

0:53 Python obtiene el siguiente valor, realiza la aritmética y agrega 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, agrega trabajo cada vez. Esta es la desensamblación 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 corresponden a la versión en pantalla, que es Python 3.10. 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 llenar 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 arreglo contiene valores de punto flotante de tamaño fijo con un tipo declarado. Nuestro arreglo es contiguo, por lo que sus valores se encuentran juntos en la memoria. Las vistas pueden tener otros diseños. La operación aritmética sabe qué tipo de datos está recibiendo. La llamada entrega el lote a una operación nativa. Un bucle aún visita 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 por lotes, las implementaciones nativas y las instrucciones vectoriales del procesador son cosas separadas. El nombre de la función proporciona muy poca evidencia.

¿Rápido significa 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 los arreglos y las 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 "kernels" compatibles. Nuestro tiempo de ejecución informa 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 solución de rendimiento que también cancela una compra. Desafortunadamente, el "benchmark" continúa.

¿Cuándo desaparece la impresionante aceleración?

2:49 Con ocho valores, la comprensión ganó. Llamar a la maquinaria del arreglo costó más que el pequeño trabajo. Arreglos listos más grandes tomaron la delantera. Utilizamos pruebas repetidas y un proceso calentado. Las compilaciones más nuevas de Python pueden cambiar el punto de inflexión. Luego cambia lo que necesita la aplicación. Comience con una lista de Python y termine con una lista de Python nueva. NumPy debe convertir la entrada en un arreglo y convertir la respuesta de nuevo en objetos

3:11 de Python. Esos pasos pertenecen a la factura si su aplicación los necesita. Con un millón de valores, esa ruta completa tomó unos veintiocho milisegundos. La comprensión también tomó unos veintiocho. Nuestra ventaja dramática se convirtió en un casi empate. La parte rápida fue real. La ventaja dependía de permitir que los datos permanecieran en su forma numérica.

¿Qué límites pertenecen al "benchmark"?

3:28 Sigue los datos. Si la siguiente operación acepta un arreglo, mantén el arreglo. Si conviertes repetidamente entre listas y arreglos, mide esos cruces. Detenerse antes de una entrega costosa da números precisos para la pregunta equivocada. Multiplicar este arreglo crea un resultado temporal. Agregar los valores originales crea la salida. La ejecución nativa todavía necesita memoria y mueve datos.

3:47 Algunas operaciones aceptan un búfer de salida, reduciendo las asignaciones cuando su 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 asignador es notablemente indiferente a tu estética.

¿Un bucle de Python compilado también puede ser rápido?

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 decorador de compilador. Todavía dice "for". La costosa ruta del intérprete ha cambiado. Después de la compilación, este bucle tomó menos de medio milisegundo con arreglos listos. La compilación en sí tomó aproximadamente un segundo en esta ejecución. Ese costo de configuración fue excluido de los tiempos repetidos,

4:22 así que un pequeño trabajo único merece una comparación diferente. La propia guía de Numba se complace en mostrar bucles explícitos. Comenzar una migración de lenguaje antes de encontrar la ruta caliente es una forma elaborada de evitar abrir un perfilador. 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 de arreglos nativos ayudan cuando el cálculo y los datos encajan.

4:43 Mide el resultado que tu llamador realmente necesita. Ahora, para ese atajo. Tienes una función de Python y quieres que acepte un arreglo. Un "wrapper" llamado "vectorize" suena como el botón de rendimiento que falta. 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 de Python para elementos sucesivos. La interfaz se hizo más corta. El límite de ejecución se mantuvo ocupado. Observa cómo los mismos valores regresan a través de la devolución de llamada de Python. Con arreglos 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 intentabas eliminar. Verifica la implementación detrás de una interfaz por lotes, mantén los datos compatibles en su forma útil e incluye la conversión en la comparación. Los bucles nativos, de devolución de llamada y compilados pueden parecer similares desde la silla del llamador. El nombre "vectorize" hizo una promesa que tu cronómetro nunca firmó. Consérvalo cuando la conveniencia sea lo que necesitas. Para el rendimiento, verifica el bucle que se ejecuta.

¿Qué deberías cambiar en tu código?

5:41 Mi veredicto es APROBARLO para el procesamiento por lotes de una ruta caliente numérica medida. Mantén los bucles ordinarios cuando ya funcionen bien. Verifica dónde ocurre el trabajo y qué necesita el siguiente llamador. Suscríbete, haz clic en la campana y dime en los comentarios si lo habrías sellado de otra manera. Y esa es la diferencia de hoy. Soy Niko de Axrisi. Fusiona con responsabilidad.

Fuentes

  1. Universal functions (ufunc) basics — NumPy 2.2NumPy
  2. The N-dimensional array — NumPy 2.2NumPy
  3. numpy.vectorize — convenience and performance notesNumPy
  4. CPU/SIMD optimizations — NumPy 2.2NumPy
  5. dis — Disassembler for Python bytecodePython Software Foundation
  6. CPython 3.10.12 list representationCPython contributors
  7. The limits of Python vectorization as a performance techniqueItamar Turner-Trauring / Python⇒Speed
  8. Performance TipsNumba
  9. Basics of CuPyCuPy

Videos relacionados