Performances Python: boucles NumPy natives, rappels et coûts de conversion
Un test numérique contrôlé, uniquement CPU, suit des valeurs identiques à travers des boucles CPython, l'arithmétique NumPy, un wrapper de rappel Python et une boucle Numba compilée.
Un test numérique contrôlé, uniquement CPU, suit des valeurs identiques à travers des boucles CPython, l'arithmétique NumPy, un wrapper de rappel Python et une boucle Numba compilée. NumPy avec entrée prête avait une grande avance, tandis que la conversion de et vers des listes Python a réduit cet avantage à une quasi-égalité. L'épisode explique la frontière d'exécution et comment mesurer le résultat dont une application a besoin.
Lire l'édition écrite (anglais) ↗
Ce que couvre cette vidéo
- CPython compile le code source en bytecode. Le travail répété de l'interpréteur/objet et une boucle numérique native typée sont des arrangements d'exécution différents.
- L'arithmétique numérique NumPy peut confier des lots typés à des opérations compilées; la boucle arithmétique existe toujours, et les vues peuvent avoir des agencements non contigus.
- Mesurez les coûts de conversion et de représentation de la sortie requise. Dans cette exécution, la conversion des listes Python en tableaux et inversement a effacé le grand avantage des tableaux prêts.
- Les petites charges de travail peuvent favoriser le Python ordinaire car les coûts fixes des appels de tableau sont importants. La compréhension à huit valeurs a gagné cette comparaison mesurée.
- np.vectorize est un wrapper de commodité qui appelle Python pour des éléments successifs; une interface de lot seule n'établit pas une exécution numérique native.
- Les boucles compilées en syntaxe Python peuvent aussi être rapides. Incluez les coûts de compilation/configuration "à froid" lorsqu'ils sont importants pour l'appelant.
- L'exécution native, le SIMD CPU, le multithreading et les backends GPU sont des idées distinctes. Les fonctionnalités CPU disponibles ne tracent pas les instructions utilisées dans une exécution particulière.
Transcription traduite
Traduit de la narration originale en anglais. L'audio et les sous-titres disponibles sont gérés par YouTube.
Pourquoi le même CPU peut-il finir plus tôt?
0:00 Vous pensez que Python a besoin d'un nouveau langage pour être rapide. Le même CPU a exécuté notre lot plus rapidement après que la boucle ait été déplacée. Où est passé le travail? Quand l'accélération disparaît-elle? Quel raccourci ramène Python? C'est The Daily Diff, sous le capot. Gardez un œil sur ce raccourci. CPython est l'implémentation standard de Python.
0:17 Il compile votre code source en bytecode, puis exécute ces instructions dans un interpréteur. NumPy est une bibliothèque numérique dont les tableaux contiennent des valeurs avec un type de données déclaré. Son arithmétique intégrée peut confier un lot entier à des opérations compilées. Prenez un million de valeurs flottantes, mettez chacune au carré, puis ajoutez la valeur originale. Chaque version reçoit des valeurs identiques et produit des réponses correspondantes. Les valeurs sont suffisamment petites pour éviter les différences d'arrondi dans la course.
0:40 Sur cette machine, la compréhension a pris environ trente-trois millisecondes. NumPy a pris moins d'une. Les deux ont tourné sur le CPU. C'est une preuve utile pour cette charge de travail, et une terrible nécrologie pour un langage de programmation. Commençons par la boucle ordinaire.
Quel travail une boucle Python répète-t-elle?
0:53 Python obtient la valeur suivante, effectue l'arithmétique, et ajoute la réponse. Puis il recommence. La multiplication est peu coûteuse. L'organisation de la multiplication suivante, et la gestion des objets Python qui l'entourent, ajoute du travail à chaque fois. Ceci est le désassemblage réel de la fonction testée. L'itération, la multiplication et l'addition apparaissent comme des instructions bytecode. Les versions et interpréteurs Python font des choix différents.
1:14 Ces timings appartiennent à la version affichée à l'écran, qui est Python trois point dix. Une liste Python stocke des références à des objets, donc différents types de choses peuvent partager une liste. Notre tâche arithmétique utilise des nombres partout. Elle paie pour un arrangement général tout en demandant un service répétitif. Imaginez commander le même café un million de fois et remplir un nouveau formulaire pour chaque tasse. La machine à café va bien.
1:36 La paperasse a développé sa propre carrière.
Où NumPy déplace-t-il la boucle?
1:38 NumPy modifie le contrat. Ce tableau contient des valeurs flottantes de taille fixe avec un type déclaré. Notre tableau est contigu, donc ses valeurs sont groupées en mémoire. Les vues peuvent avoir d'autres agencements. L'opération arithmétique sait quel type de données elle reçoit. L'appel transmet le lot à une opération native. Une boucle visite toujours les valeurs à l'intérieur du code compilé. Elle peut réutiliser la connaissance du type à travers ce travail.
2:00 Python reste la personne qui confie la tâche. Itamar Turner Trauring explique la distinction dans Python Speed. Les interfaces de lot, les implémentations natives et les instructions vectorielles du processeur sont des choses distinctes. Le nom de la fonction fournit très peu de preuves.
Rapide signifie-t-il GPU ou plusieurs cœurs CPU?
2:13 Ce résultat a utilisé NumPy sur le CPU. Un backend GPU tel que CuPy choisit un domicile différent pour les tableaux et les opérations. L'achat d'un GPU laisse cette boucle Python ordinaire s'exécuter là où elle s'exécutait déjà. Les instructions vectorielles du CPU peuvent traiter plusieurs valeurs ensemble. NumPy dispose de mécanismes pour choisir les noyaux pris en charge. Notre temps d'exécution signale les fonctionnalités disponibles, mais ne trace pas les instructions de cette expression. Attribuer tout le gain mesuré aux instructions vectorielles survendraient les preuves.
2:40 Le panier d'achat du GPU peut rester fermé. Il y a quelque chose de profondément satisfaisant dans une solution de performance qui annule aussi un achat. Malheureusement, le benchmark continue.
Quand l'impressionnant gain de vitesse disparaît-il?
2:49 Avec huit valeurs, la compréhension a gagné. Appeler la machinerie du tableau a coûté plus cher que la minuscule tâche. Les tableaux prêts plus grands ont pris de l'avance. Nous avons utilisé des essais répétés et un processus "à chaud". Les nouvelles versions de Python peuvent modifier le point d'inflexion. Ensuite, changez ce dont l'application a besoin. Commencez avec une liste Python et terminez avec une nouvelle liste Python. NumPy doit convertir l'entrée en un tableau et reconvertir la réponse en objets
3:11 Python. Ces étapes doivent être facturées si votre application en a besoin. Pour un million de valeurs, ce chemin complet a pris environ vingt-huit millisecondes. La compréhension a pris environ vingt-huit aussi. Notre avance spectaculaire est devenue une quasi-égalité. La partie rapide était réelle. L'avantage dépendait de la conservation des données sous leur forme numérique.
Quelles frontières appartiennent au benchmark?
3:28 Suivez les données. Si la prochaine opération accepte un tableau, gardez le tableau. Si vous convertissez à plusieurs reprises entre listes et tableaux, mesurez ces passages. S'arrêter avant une transition coûteuse donne des chiffres précis pour la mauvaise question. Multiplier ce tableau crée un résultat temporaire. Ajouter les valeurs originales crée la sortie. L'exécution native a toujours besoin de mémoire et déplace les données.
3:47 Certaines opérations acceptent un tampon de sortie, réduisant les allocations lorsque votre code peut le réutiliser en toute sécurité. Une seule jolie ligne peut commander plusieurs grands tampons. Python Speed signale également ce problème. L'allocateur est remarquablement indifférent à votre esthétique.
Une boucle Python compilée peut-elle être rapide aussi?
4:00 Vous pouvez également conserver la syntaxe de boucle et modifier son exécution. Numba peut compiler les fonctions numériques Python prises en charge en code natif. Voici la boucle de notre test avec son décorateur de compilateur. Elle dit toujours for. Le chemin coûteux de l'interpréteur a changé. Après compilation, cette boucle a pris moins d'une demi-milliseconde avec des tableaux prêts. La compilation elle-même a pris environ une seconde dans cette exécution. Ce coût de configuration a été exclu des mesures répétées,
4:22 donc un minuscule travail ponctuel mérite une comparaison différente. Le propre guide de Numba est heureux de montrer des boucles explicites. Commencer une migration de langage avant de trouver le chemin chaud est une façon élaborée d' éviter d'ouvrir un profileur. Trouvez d'abord ce qui consomme le temps. Les attentes réseau, l'analyse d'objets et l'arithmétique de masse appellent des correctifs différents. Les opérations de tableau natives aident lorsque le calcul et les données correspondent.
4:43 Mesurez le résultat dont votre appelant a réellement besoin. Maintenant, pour ce raccourci. Vous avez une fonction Python et vous voulez qu'elle accepte un tableau. Un wrapper nommé vectorize semble être le bouton de performance manquant. Sa documentation contient une phrase importante qui attend.
Qu'exécute réellement np.vectorize?
4:56 NumPy dit que son wrapper vectorize est fourni principalement pour la commodité. Son implémentation est essentiellement une boucle for. Le wrapper accepte les lots, mais continue d'appeler votre fonction Python pour les éléments successifs. L'interface est devenue plus courte. La frontière d'exécution est restée occupée. Regardez les mêmes valeurs revenir via le callback Python. Avec des tableaux prêts, cette version a pris environ quatre-vingt-sept millisecondes. Elle était plus lente que notre compréhension.
5:18 Un wrapper de "broadcasting" utile peut préserver le travail exact par élément que vous essayiez de supprimer. Vérifiez l'implémentation derrière une interface de lot, conservez les données compatibles dans leur forme utile, et incluez la conversion dans la comparaison. Les boucles natives, de rappel et compilées peuvent sembler similaires du point de vue de l'appelant. Le nom vectorize a fait une promesse que votre chronomètre n'a jamais signée. Gardez-le lorsque la commodité est ce dont vous avez besoin. Pour la performance, vérifiez la boucle qui s'exécute.
Que devriez-vous changer dans votre code?
5:41 Mon verdict est SHIP IT pour le traitement par lots d'un chemin critique numérique mesuré. Gardez les boucles ordinaires quand elles fonctionnent déjà bien. Vérifiez où le travail se produit et ce dont le prochain appelant a besoin. Abonnez-vous, appuyez sur la cloche, et dites-moi dans les commentaires si vous l'auriez estampillé différemment. Et c'est le diff pour aujourd'hui. Je suis Niko d'Axrisi. Fusionnez de manière responsable.
Sources
- 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



