Maison  >  Article  >  développement back-end  >  99% des gens ne le savent pas ! Python, C, extensions C, comparaison des différences Cython !

99% des gens ne le savent pas ! Python, C, extensions C, comparaison des différences Cython !

WBOY
WBOYavant
2023-04-14 17:40:031968parcourir

99% des gens ne le savent pas ! Python, C, extensions C, comparaison des différences Cython !

Prenons la simple séquence de Fibonacci comme exemple pour tester la différence dans leur efficacité d'exécution.

Code Python :

def fib(n):
a, b = 0.0, 1.0
for i in range(n):
a, b = a + b, a
return a

Code C :

double cfib(int n) {
int i;
double a=0.0, b=1.0, tmp;
for (i=0; i<n; ++i) {
tmp = a; a = a + b; b = tmp;
}
return a;
}

Ce qui précède est une séquence de Fibonacci implémentée en C. Certaines personnes peuvent être curieuses de savoir pourquoi nous utilisons une virgule flottante au lieu d'un entier. Qu'en est-il du type ? La réponse est que le type entier de C a une plage, nous utilisons donc double, et le float de Python correspond à PyFloatObject dans la couche inférieure, qui est également stocké en interne par double.

Extension C :

Ensuite, l'extension C, remarque : l'extension C n'est pas notre objectif, l'écriture de l'extension C et l'écriture de Cython sont essentiellement les mêmes, les deux écrivent des modules d'extension pour Python, mais l'écriture de Cython est absolument Beaucoup plus simple que d'écrire une extension C.

#include "Python.h"

double cfib(int n) {
int i;
double a=0.0, b=1.0, tmp;
for (i=0; i<n; ++i) {
tmp = a; a = a + b; b = tmp;
}
return a;
}

static PyObject *fib(PyObject *self, PyObject *n) {
if (!PyLong_CheckExact(n)) {
wchar_t *error = L"函数 fib 需要接收一个整数";
PyErr_SetObject(PyExc_ValueError,
PyUnicode_FromWideChar(error, wcslen(error)));
return NULL;
}
double result = cfib(PyLong_AsLong(n));
return PyFloat_FromDouble(result);
}

static PyMethodDef methods[] = {
{"fib",
 (PyCFunction) fib,
 METH_O,
 "这是 fib 函数"},
 {NULL, NULL, 0, NULL}
};

static PyModuleDef module = {
PyModuleDef_HEAD_INIT,
"c_extension",
"这是模块 c_extension",
-1,
methods,
NULL, NULL, NULL, NULL
};

PyMODINIT_FUNC PyInit_c_extension(void) {
return PyModule_Create(&module);
}

Vous pouvez voir que si vous écrivez une extension C, même un simple Fibonacci est très compliqué.

Code Cython :

Enfin, voyons comment utiliser Cython pour écrire Fibonacci. À votre avis, à quoi devrait ressembler le code écrit en utilisant Cython ?

def fib(int n):
cdef int i
cdef double a = 0.0, b = 1.0
for i in range(n):
a, b = a + b, a
return a

Et si les codes Cython et les codes Python sont très similaires ? Bien que nous n'ayons pas encore formellement appris la syntaxe de Cython, vous devriez pouvoir deviner ce que signifie le code ci-dessus. Nous avons défini une variable de niveau C à l'aide du mot-clé cdef et déclaré leur type.

Le code Cython doit être compilé dans un module d'extension avant de pouvoir être reconnu par l'interpréteur, il doit donc d'abord être traduit en code C puis compilé dans un module d'extension. Encore une fois, il n'y a essentiellement aucune différence entre l'écriture d'extensions C et l'écriture de code Cython qui doit également être traduit en code C.

Mais il est évident qu'écrire Cython est beaucoup plus simple que d'écrire des extensions C. Si la qualité du code Cython écrit est élevée, la qualité du code C traduit sera également élevée et un traitement maximal sera automatiquement effectué pendant. le degré d’optimisation du processus de traduction. Mais s’il s’agit d’une extension C manuscrite, alors toutes les optimisations doivent être gérées manuellement par le développeur, sans compter que lorsque les fonctions sont complexes, écrire des extensions C en soi est un casse-tête.

Pourquoi Cython peut-il accélérer ?

En regardant le code Cython, comparé au code Python Fibonacci pur, nous voyons que la différence semble être que les types de variables i, a et b ont été spécifiés à l'avance. La clé est pourquoi cela peut accélérer. Qu'en est-il de l'effet (bien qu'il n'ait pas encore été testé, la vitesse va certainement augmenter, sinon il n'est pas nécessaire d'apprendre Cython).

Mais la raison est ici, car toutes les variables en Python sont un pointeur générique PyObject*. PyObject (une structure en C) a deux membres internes, à savoir ob_refcnt : contient le nombre de références de l'objet, ob_type * : contient le pointeur du type d'objet.

Qu'il s'agisse d'un entier, d'un nombre à virgule flottante, d'une chaîne, d'un tuple, d'un dictionnaire ou de toute autre chose, toutes les variables pointant vers eux sont un PyObject *. Lors de l'utilisation, vous devez d'abord obtenir le pointeur du type correspondant via -> ob_type puis effectuer la conversion.

Par exemple, a et b dans le code Python, nous savons que quel que soit le niveau de boucle effectué, le résultat pointe vers un nombre à virgule flottante, mais l'interpréteur ne fera pas cette inférence. Chaque ajout doit être détecté pour déterminer de quel type il s'agit et converti ; puis lorsque l'ajout est effectué, accédez à la méthode interne __add__ pour ajouter les deux objets et créez un nouvel objet une fois l'exécution terminée, convertissez le pointeur vers ce nouveau ; objet à PyObject * et retour.

Et les objets Python allouent de l'espace sur le tas, et a et b sont immuables, donc chaque cycle créera un nouvel objet et recyclera l'objet précédent.

Tout ce qui précède entraîne l'impossibilité d'une efficacité d'exécution élevée du code Python. Bien que Python fournisse également un pool de mémoire et un mécanisme de mise en cache correspondant, il est évidemment toujours incapable de résister à la faible efficacité.

Quant à la raison pour laquelle Cython peut accélérer, nous en reparlerons plus tard.

Différence d'efficacité

Alors, quelle est la différence d'efficacité entre eux ? Utilisons un tableau pour comparer :

99% des gens ne le savent pas ! Python, C, extensions C, comparaison des différences Cython !

Le facteur d'amélioration fait référence au nombre de fois où l'efficacité est améliorée par rapport au Python pur.

La deuxième colonne est fib(0). Évidemment, elle n'entre pas réellement dans la boucle. fib(0) mesure le coût d'appel d'une fonction. L'avant-dernière colonne « Consommation de temps du corps de la boucle » fait référence au temps passé à exécuter le corps de la boucle interne lors de l'exécution de fib(90), à l'exclusion de la surcharge de l'appel de fonction lui-même.

Dans l'ensemble, Fibonacci écrit en langage C pur est sans aucun doute le plus rapide, mais il y a beaucoup de choses qui méritent d'être réfléchies.

Pure Python

Comme prévu, c'est celui avec les pires performances dans tous les aspects. À en juger par fib(0), l'appel d'une fonction prend 590 nanosecondes, ce qui est beaucoup plus lent que C. La raison en est que Python doit créer un cadre de pile lors de l'appel d'une fonction, et ce cadre de pile est alloué sur le tas, et après. à la fin, cela implique également la destruction des cadres de pile, etc. Quant à fib(90), aucune analyse n’est évidemment nécessaire.

Pure C

Évidemment, il n'y a aucune interaction avec le runtime Python pour le moment, donc la consommation de performances est minime. fib(0) montre que C appelle une fonction avec une surcharge de seulement 2 nanosecondes ; fib(90) montre que C est près de 80 fois plus rapide que Python lors de l'exécution d'une boucle.

Extension C

Comme mentionné ci-dessus, l'extension C consiste à utiliser C pour écrire des modules d'extension pour Python. Nous examinons la consommation de temps du corps de la boucle et constatons que l'extension C est presque la même que le C pur. La différence est que plus de temps est consacré aux appels de fonction. La raison en est que lorsque nous appelons la fonction du module d'extension, nous devons d'abord convertir les données Python en données C, puis utiliser la fonction C pour calculer la séquence de Fibonacci, puis convertir les données C en données Python.

L'extension C est donc essentiellement un langage C, mais elle doit suivre la spécification API fournie par CPython lors de l'écriture, afin que le code C puisse être compilé dans un fichier pyd et directement appelé par Python. Du point de vue des résultats, c'est la même chose que Cython. Mais encore une fois, écrire des extensions en C revient essentiellement à écrire du C, et vous devez également être familier avec l'API Python/C sous-jacente, ce qui est relativement difficile.

Cython

Si vous regardez le temps nécessaire au corps de la boucle seul, le C pur, l'extension C et Cython sont tous à peu près identiques, mais écrire Cython est évidemment le plus pratique. Nous disons que ce que fait Cython est essentiellement similaire aux extensions C. Ils fournissent tous deux des modules d'extension pour Python. La différence est la suivante : l'un consiste à écrire manuellement du code C et l'autre à écrire du code Cython, puis à le traduire automatiquement en code C. Par conséquent, pour Cython, le processus de conversion des données Python en données C, d'exécution de calculs, puis de reconversion des données Python est inévitable.

Mais nous constatons que Cython prend beaucoup moins de temps pour appeler des fonctions que les extensions C. La raison principale est que le code C généré par Cython est hautement optimisé. Mais pour être honnête, nous n’avons pas besoin de trop nous soucier du temps nécessaire aux appels de fonction. Ce à quoi nous devons prêter attention, c’est le temps nécessaire à l’exécution des blocs de code internes. Bien sûr, nous parlerons également plus tard de la façon de réduire la surcharge de l’appel de fonction lui-même.

Pourquoi la boucle for de Python est-elle si lente ?

Nous pouvons voir d'après la consommation de temps du corps de la boucle que la boucle for de Python est vraiment notoirement lente, alors quelle en est la raison ? Analysons-le.

1. Mécanisme de boucle for de Python

Lorsque Python traverse un objet itérable, il appelle d'abord la méthode __iter__ à l'intérieur de l'objet itérable pour renvoyer son itérateur correspondant, puis appelle en continu la méthode __next__ de l'itérateur ; itère les valeurs une par une jusqu'à ce que l'itérateur lève une exception StopIteration, qui est capturée par la boucle for et termine la boucle.

Et les itérateurs sont avec état, et l'interpréteur Python doit enregistrer l'état d'itération de l'itérateur à tout moment.

2.Opérations arithmétiques en Python

Nous l'avons mentionné ci-dessus En raison de ses propres caractéristiques dynamiques, Python ne peut effectuer aucune optimisation basée sur le type.

Par exemple : a + b dans le corps de la boucle, ces a et b peuvent pointer vers des entiers, des nombres à virgule flottante, des chaînes, des tuples, des listes, ou même des objets d'instance de la classe où nous avons implémenté la méthode magique __add__. Attends, attends.

Bien que nous sachions qu'il s'agit d'un nombre à virgule flottante, Python ne fait pas cette hypothèse, donc chaque fois que a + b est exécuté, quel est son type ? Déterminez ensuite s'il existe une méthode __add__ en interne. Si tel est le cas, effectuez un appel avec a et b comme paramètres pour ajouter les objets pointés par a et b. Une fois le résultat calculé, son pointeur est converti en PyObject * et renvoyé.

Pour C et Cython, lors de la création d'une variable, le type est spécifié à l'avance comme double, pas autres, donc le a + b compilé n'est qu'une simple instruction machine. En comparaison, comment Python peut-il ne pas être lent ?

3. Allocation de mémoire des objets Python

Les objets Python sont alloués sur le tas, car les objets Python sont essentiellement un morceau de mémoire alloué dans la zone du tas pour la structure par la fonction malloc de C. L'allocation et la libération de mémoire dans la zone du tas nécessitent beaucoup d'argent, mais la pile est beaucoup plus petite, elle est maintenue par le système d'exploitation et sera automatiquement recyclée, ce qui est extrêmement efficace pour l'allocation et la libération de mémoire sur la pile uniquement. nécessite un seul mouvement. Juste un registre.

Mais le tas n'a évidemment pas ce traitement, et les objets Python sont tous alloués sur le tas, bien que Python introduise un mécanisme de pool de mémoire pour éviter dans une certaine mesure les interactions fréquentes avec le système d'exploitation, et introduise également de petits objets entiers. pool, mécanisme interne de chaîne, pool de cache, etc.

Mais en fait, lorsqu'il s'agit de création et de destruction d'objets (n'importe quel objet, y compris les scalaires), cela augmentera la surcharge de mémoire allouée dynamiquement et du sous-système de mémoire de Python. L'objet float est immuable, il sera donc créé et détruit à chaque boucle, donc l'efficacité n'est toujours pas élevée.

Les variables allouées par Cython (lorsque le type est un type en C), ce ne sont plus des pointeurs (les variables Python sont toutes des pointeurs), pour les a et b courants, elles sont allouées sur la pile Virgule flottante double précision nombre. L'efficacité de l'allocation sur la pile est bien supérieure à celle du tas, elle est donc très adaptée aux boucles for, donc l'efficacité est bien supérieure à celle de Python. De plus, non seulement lors de l'allocation, mais également lors de l'adressage, la pile est plus efficace que le tas.

Il n'est donc pas surprenant que C et Cython soient des ordres de grandeur plus rapides que Python pur en ce qui concerne les boucles for, puisque Python fait beaucoup de travail à chaque itération.

Quand utiliser Cython ?

Nous voyons que dans le code Cython, le simple ajout de quelques cdefs peut permettre une si grande amélioration des performances, ce qui est évidemment très excitant. Cependant, tous les codes Python ne connaîtront pas d’énormes améliorations de performances lorsqu’ils seront écrits en Cython.

L'exemple de séquence de Fibonacci que nous avons ici est délibéré, car les données à l'intérieur sont liées au CPU et le temps d'exécution est consacré au traitement de certaines variables dans le registre du CPU, sans qu'il soit nécessaire de déplacer les données. Si cette fonction effectue le travail suivant :

  • gourmand en mémoire, comme l'ajout d'éléments à un grand tableau ;
  • gourmand en E/S, comme la lecture de fichiers volumineux à partir du disque ; car la lecture à partir du disque du serveur FTP télécharge des fichiers
  • Ensuite, les différences entre Python, C, Cython peuvent être considérablement réduites (pour les opérations gourmandes en stockage) voire disparaître complètement (pour les opérations gourmandes en E/S ou en réseau) -opérations intensives).

Lorsque notre objectif est d'améliorer les performances des programmes Python, le principe de Pareto nous aide beaucoup, c'est-à-dire : 80 % du temps d'exécution du programme est causé par 20 % du code. Mais sans une analyse minutieuse, il est difficile de trouver ces 20 % de code. Par conséquent, avant d'utiliser Cython pour améliorer les performances, l'analyse de la logique métier globale est la première étape.

Si nous déterminons par analyse que le goulot d'étranglement du programme est causé par les E/S du réseau, nous ne pouvons pas nous attendre à ce que Cython apporte des améliorations significatives des performances. Par conséquent, avant d’utiliser Cython, il est nécessaire de déterminer d’abord la cause du goulot d’étranglement dans le programme. Ainsi, même si Cython est un outil puissant, il doit être utilisé de la bonne manière.

De plus, Cython a introduit le système de types C dans Python, nous devons donc prêter attention aux limitations des types de données C. Nous savons que les entiers de Python ne sont pas limités par la longueur, mais les entiers de C sont restreints, ce qui signifie qu'ils ne peuvent pas représenter correctement les entiers d'une précision infinie.

Cependant, certaines fonctionnalités de Cython peuvent nous aider à intercepter ces débordements. En bref, le plus important est : les types de données C sont plus rapides que les types de données Python, mais ils sont soumis à des limitations qui les rendent moins flexibles et polyvalents. De là, nous pouvons également voir que Python a choisi ce dernier en termes de vitesse, de flexibilité et de polyvalence.

Considérez également une autre fonctionnalité de Cython : la connexion à du code externe. Supposons que notre point de départ ne soit pas Python, mais C ou C++, et que nous souhaitions utiliser Python pour connecter plusieurs modules C ou C++. Cython comprend les déclarations C et C++ et peut générer du code hautement optimisé, il convient donc mieux comme pont.

Comme je suis un maître Python, s'il s'agit de C et C++, je présenterai comment introduire C et C++ dans Cython et appeler directement la bibliothèque C déjà écrite. Il ne présentera pas comment introduire Cython dans C et C++ comme pont pour connecter plusieurs modules C et C++. J'espère que vous comprenez cela, car je n'utilise pas C ou C++ pour écrire des services, je les utiliserai uniquement pour aider Python à améliorer son efficacité.

Résumé

Jusqu'à présent, je n'ai présenté que Cython, et j'ai principalement discuté de son positionnement et de ses différences avec Python et C. Quant à savoir comment utiliser Cython pour accélérer Python, comment écrire du code Cython et sa syntaxe détaillée, nous le présenterons plus tard.

En bref, Cython est un langage mature qui sert Python. Le code Cython ne peut pas être exécuté directement car il n'est pas conforme aux règles de syntaxe de Python.

La façon dont nous utilisons Cython est la suivante : traduisez d'abord le code Cython en code C, puis compilez le code C dans un module d'extension (fichier pyd), puis importez-le dans le code Python et appelez les méthodes fonctionnelles à l'intérieur. ce que nous faisons La manière correcte, et certainement la seule, d'utiliser Cython.

Par exemple, le Fibonacci que nous avons écrit dans Cython ci-dessus signalera une erreur s'il est exécuté directement, car cdef n'est évidemment pas conforme aux règles de syntaxe de Python. Par conséquent, le code Cython doit être compilé dans un module d'extension puis importé dans un fichier py ordinaire. L'importance de ceci est d'améliorer la vitesse d'exécution. Par conséquent, les codes Cython doivent être des codes gourmands en CPU, sinon il sera difficile d'améliorer considérablement l'efficacité.

Donc avant d'utiliser Cython, il est préférable d'analyser soigneusement la logique métier, ou de ne pas utiliser Cython pour le moment et de l'écrire entièrement en Python. Une fois l'écriture terminée, commencez à tester et à analyser les performances du programme pour voir où cela prend plus de temps, mais en même temps, il peut être optimisé grâce au typage statique. Trouvez-les, réécrivez-les dans Cython, compilez-les dans des modules d'extension, puis appelez les fonctions dans le module d'extension.

Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!

Déclaration:
Cet article est reproduit dans:. en cas de violation, veuillez contacter admin@php.cn Supprimer