+− THE DAILY DIFFdev & AI news
SHIP IT

Performance 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.

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

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 limite d'exécution et comment mesurer le résultat dont une application a besoin.

Lire l'édition écrite (anglais) ↗

Ce que cette vidéo couvre

  • 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 transmettre 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 la conversion et la représentation de sortie requise. Dans cette exécution, la conversion des listes Python en tableaux et vice-versa 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 tableaux sont importants. La compréhension à huit valeurs a remporté cette comparaison mesurée.
  • np.vectorize est un wrapper de commodité qui appelle Python pour des éléments successifs; une interface de lot ne suffit pas à établir 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" quand 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 contrôlés par YouTube.

Pourquoi le même CPU peut-il terminer 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, dans les coulisses. 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 passer 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 obtient 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 fonctionné 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 bon marché. Organiser la prochaine multiplication, et gérer les objets Python autour d'elle, ajoute du travail à chaque fois. C'est le désassemblage réel de la fonction testée. L'itération, la multiplication et l'addition apparaissent comme des instructions en bytecode. Les versions et interpréteurs Python font des choix différents.

1:14 Ces temps appartiennent à la version à l'écran, qui est Python trois point dix. Une liste Python stocke des références à des objets, de sorte que 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 paperasserie a développé sa propre carrière.

Où NumPy déplace-t-il la boucle?

1:38 NumPy change le contrat. Ce tableau contient des valeurs flottantes de taille fixe avec un type déclaré. Notre tableau est contigu, donc ses valeurs sont regroupées en mémoire. Les vues peuvent avoir d'autres dispositions. 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 le travail. Itamar Turner Trauring explique la distinction dans Python Speed. Les interfaces de lots, 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 supportés. Notre exécution rapporte les fonctionnalités disponibles, mais ne trace pas les instructions de cette expression. Attribuer l'intégralité du gain mesuré aux instructions vectorielles serait survendre la preuve.

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 également un achat. Malheureusement, le benchmark continue.

Quand l'impressionnante accélération disparaît-elle?

2:49 Avec huit valeurs, la compréhension a gagné. L'appel de la machinerie des tableaux a coûté plus cher que la petite 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 chauffé. Les versions plus récentes de Python peuvent modifier le point de croisement. Alors, changez ce dont l'application a besoin. Commencez par une liste Python et terminez par 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 incluses dans la facture 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 avantage dramatique est devenu un quasi-égalité. La partie rapide était réelle. L'avantage dépendait du fait de laisser les données sous leur forme numérique.

Quelles limites appartiennent au benchmark?

3:28 Suivez les données. Si l'opération suivante accepte un tableau, conservez le tableau. Si vous convertissez à plusieurs reprises entre listes et tableaux, mesurez ces croisements. S'arrêter avant un transfert coûteux 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 nécessite toujours de la mémoire et déplace des 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 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 aussi être rapide?

4:00 Vous pouvez également conserver la syntaxe de la boucle et en modifier l'exécution. Numba peut compiler les fonctions numériques Python supportées en code natif. Voici la boucle de notre test avec son décorateur de compilateur. Il indique 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 petit travail unique mérite une comparaison différente. Le guide de Numba est heureux de montrer des boucles explicites. Commencer une migration de langage avant de trouver le chemin critique est une manière élaborée d' éviter d'ouvrir un profileur. Trouvez d'abord ce qui consomme du temps. Les attentes réseau, l'analyse d'objets et l'arithmétique de masse appellent des solutions différentes. 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 voulez qu'elle accepte un tableau. Un wrapper nommé vectorize ressemble au bouton de performance manquant. Sa documentation contient une phrase importante qui attend.

Qu'est-ce que np.vectorize exécute réellement?

4:56 NumPy indique que son wrapper vectorize est fourni principalement par commodité. Son implémentation est essentiellement une boucle for. Le wrapper accepte des lots, mais continue d'appeler votre fonction Python pour les éléments successifs. L'interface est devenue plus courte. La limite d'exécution est restée occupée. Regardez les mêmes valeurs revenir via le rappel 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 diffusion 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 sous 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 la différence pour aujourd'hui. Je suis Niko d'Axrisi. Fusionnez de manière responsable.

Sources

  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

Vidéos similaires