Les langages «Python» classés comme dynamiquement typés incluent «Cython» qui convertit le code Python en C / C ++ et le compile, et «Numba» qui accélère Python à l'aide d'un compilateur JIT. Je voulais améliorer l'efficacité en les incorporant dans le code Python utilisé dans la recherche et les stages, et afin d'approfondir ma compréhension, j'ai essayé de comparer et de vérifier la vitesse de traitement pour un traitement simple réel sur jupyter. ..
Cet article n'est pas un article d'introduction sur Cython. Qu'est-ce que Cython? Si vous souhaitez connaître les connaissances de base et l'utilisation, nous vous recommandons les articles suivants.
Si vous avez des opinions ou des erreurs, n'hésitez pas à commenter. Le code expérimental est ici
J'ai essayé d'augmenter la vitesse en utilisant la fonction simple ʻis_prime` (fonction de jugement principal) ci-dessous comme base. À propos du code La même partie que la ligne de base est omise. Concernant Cython, nous avons vérifié la différence de vitesse en fonction de l'emplacement du type.
baseline
def find_factor(n):
answer = n
for i in range (2,n):
if n % i == 0:
answer = i
break
return answer
def is_prime(n):
if find_factor (n) != n:
return False
else:
return True
from numba import jit
@jit
def find_factor0 (n):
#Abréviation
def is_prime0 (n):
#Abréviation
Compilez simplement avec Cython sans changer le code
% load_ext Cython
%% cython
def find_factor1(n):
#Abréviation
def is_prime1(n):
#Abréviation
Tapez toutes les variables
% load_ext Cython
%% cython
def find_factor2(int n):
cdef int answer = n
cdef int i
#Abréviation
def is_prime2(n):
#Abréviation
Type-définir seulement i dans l'instruction for
% load_ext Cython
%% cython
def find_factor2_1(n):
answer = n
cdef int i
#Abréviation
def is_prime2_1(n):
#Abréviation
Tapez uniquement la réponse
% load_ext Cython
%% cython
def find_factor2_2(n):
cdef int answer = n
#Abréviation
def is_prime2_2(n):
#Abréviation
Définition de type uniquement argument n
% load_ext Cython
%% cython
def find_factor2_3(int n):
#Abréviation
def is_prime2_3(int n):
#Abréviation
Définition de type autre que l'argument n
% load_ext Cython
%% cython
def find_factor2_4(n):
cdef int answer = n
cdef int i
#Abréviation
def is_prime2_4(n):
#Abréviation
Définissez la fonction findfactor avec cdef
% load_ext Cython
%% cython
cdef int find_factor3(int n):
cdef int answer = n
cdef int i
#Abréviation
def is_prime3(n):
#Abréviation
Dans toutes les expériences, le temps nécessaire pour déterminer le nombre premier 131071 a été mesuré par "% timeit". Le temps moyen et l'écart type de chaque processus exécuté 100 fois sont les suivants.
Baseline | Numba | Cython1 Aucun changement de code |
Cython2 La fonction elle-même |
Cython3 Toutes les variables de la fonction |
|
---|---|---|---|---|---|
temps(ms) | 8.69±0.2 | 0.49±0.0 | 5.34±0.1 | 0.43±0.0 | 0.43±0.0 |
Cython2-1 pour la déclaration i seulement |
Cython2-2 réponse seulement |
Cython2-3 Argument n uniquement |
Cython2-4 Autre que l'argument n |
|
---|---|---|---|---|
temps(ms) | 4.81±0.1 | 5.27±0.4 | 6.62±0.1 | 4.85±0.1 |
D'après le résultat de Cython2, c'est le plus rapide pour Cython lorsque toutes les informations de type sont définies. Considérons maintenant le changement de vitesse dû à la différence dans la définition du type. Comme on peut le voir à partir de la différence entre Cython2-1 et Cython2-2, l'effet des variables de définition de type est largement dû à la première (variable d'itérateur de définition de type ʻi). Cela est dû au fait que le calcul d'incrément effectué dans la boucle for a un contrôle de type pour chaque opération (équivalent au processus d'appel de ʻint .__ add__
à chaque fois), donc s'il est beaucoup exécuté, la surcharge sera élevée, mais la définition de type On pense que cette surcharge est éliminée en calculant ʻi + 1` directement sur le langage C.
De plus, en faisant attention à Cython2-3, la définition de type avec uniquement des arguments est plus lente que Cython1. Je pensais que cela devrait être plus rapide en supprimant la vérification de type dynamique dans les arguments lors de l'appel de la fonction find factor
, mais c'était en fait plus lent. Considérant qu'il y a une surcharge de compilation due à la définition du type d'argument plutôt qu'à ces effets, nous avons défini les fonctions suivantes hoge1, hoge2
, qui ne font que différer uniquement par la présence ou l'absence de la définition du type d'argument, et comparé les vitesses. Cependant, il y avait une légère différence.
%% cython
def hoge1 (n,a,b,c):
return
def hoge2 (int n, int a, int b, int c):
return
Cependant, même avec la fonction ci-dessus, la différence était d'environ 10 ns (10 millions de boucles sont tournées et la différence ne devrait pas être une erreur par rapport à la valeur de l'écart type), et en plus, il n'y avait presque pas de différence lorsqu'il y avait un argument, donc cette surcharge On ne considère pas que la différence est due à. Vous pouvez également voir à partir de Cython2-4 qu'il est plus rapide de taper les arguments si toutes les variables du find factor
sont tapées. D'après ce qui précède, la cause du retard en Cython2-3 est la dynamique lorsqu'une seule des opérations «n% i == 0» est tapée que lorsque ni «n ni i» ne sont tapés. Je pense que la conclusion est que les frais généraux du chèque sont plus importants, mais je n'ai pas compris la raison.
Le résultat de la saisie du find factor
lui-même à partir de Cython3 n'était pas très différent de Cython2. Le traitement arithmétique lui-même dans find factor
est le même que Cython2, donc je suis convaincu. Cependant, en Cython3, la surcharge lors de l'appel de find factor
devrait être supprimée, donc appeler find factor
une fois ne fait presque aucune différence, mais je pense que Cython3 sera plus rapide dans le processus d'appels de beaucoup. J'ai créé la fonction suivante et l'ai vérifiée.
%% cython
cdef int find_factor3_1(int n):
#Abréviation
def find_factor3_2(int n):
#Abréviation
def is_prime_inf1(n,m):
for i in range (m):
find_factor3_1 (n)
def is_prime_inf2(n,m):
for i in range (m):
find_factor3_2 (n)
find factor3-1(cdef) | find factor3-2(def) | |
---|---|---|
temps | 158 μs | 3.1s |
Une différence significative est apparue en appelant un grand nombre de «facteurs de recherche» comme prévu. Par conséquent, il a été constaté que la surcharge de l'appel en définissant le «facteur de recherche» lui-même est éliminée.
J'ai essayé d'augmenter la vitesse avec le produit matriciel comme base de référence.
def dot(a, b):
n = len(a)
l = len(a[0])
m = len(b[0])
c = np.zeros((n, m))
for i in range(n):
for j in range(m):
for k in range(l):
c[i][j] += a[i][k] * b[k][j]
return c
from numba import jit
@jit
def nm_dot(a, b):
#Abréviation
Pour le moment, la définition de type est effectuée sur la base des connaissances à ce jour et implémentée avec cython. Changez en [i] [j]
→ [i, j]
pour accélérer, déclarez les définitions de type ensemble, changez en range
→ xrange
, désactivez la fonction de vérification, etc. Nous concevons.
%%cython
import numpy as np
cimport numpy as np
cython: boundscheck=False
cython: wraparound=False
cpdef np.ndarray c_dot(np.ndarray a, np.ndarray b):
cdef np.ndarray c
cdef int n,l,m,i,j,k
n = len(a)
l = len(a[0])
m = len(b[0])
c = np.zeros((n, m))
for i in xrange(n):
for j in xrange(m):
for k in xrange(l):
c[i,j] += a[i,k] * b[k,j]
return c
La définition de type pour le tableau numpy a été rendue plus rigoureuse en référence à la documentation officielle.
cpdef np.ndarray[dtype=float,ndim = 2] c_dot2(np.ndarray[dtype=float,ndim = 2] a, np.ndarray[dtype=float,ndim = 2] b):
cdef np.ndarray[dtype=double,ndim = 2] c
#Abréviation
Dans cette expérience, une matrice Numpy aléatoire de 100 * 100 a été générée et mesurée avec % timeit
comme dans l'expérience 1. La moyenne et les écarts-types pour 10 processus dans l'implémentation chronophage de Python et Cython 1 et 100 processus dans les autres cas sont les suivants.
Python | Python(Numpy) | Numba | Cython1 | Cython2 | |
---|---|---|---|---|---|
temps(ms) | 1220±54 | 0.033 | 1.1 | 729±16 | 2.5 |
C'est un peu plus rapide en Cython1 que l'implémentation naïve de Python, mais beaucoup plus rapide en Cython2. Dans l'implémentation en Cython1, lorsque le type de chaque tableau numpy est défini comme np.ndarray, le dtype et la dimension réels ne sont pas encore déterminés et il est soumis à une vérification dynamique après tout, ce qui équivaut au traitement lorsque Cython n'est pas utilisé. On pense que c'est parce qu'il est stocké. Comme vous pouvez le voir à partir des résultats, Numpy a été extrêmement rapide lorsqu'il s'agissait de calculer le produit matriciel. En premier lieu, j'ai beaucoup utilisé Numpy, mais je ne connaissais pas le principe pourquoi c'était si rapide, alors j'ai fait un peu de recherche (j'aurais dû le faire en premier). , Il a été dit qu'il a été implémenté en langage C en interne. Ici aussi, la frappe est sortie et j'ai réalisé l'importance de la frappe. Il est préférable d'utiliser numpy tel quel, au moins pour les calculs implémentés en tant que modules dans numpy tels que les produits matriciels, et Cython lorsqu'il n'est pas implémenté en tant que module numpy et ne peut pas être réalisé sans accéder réellement au tableau numpy plusieurs fois Cela semble bon à utiliser.
Grâce à cette expérience, l'introduction pratique de Cython ne tape pas dans les nuages sombres, mais améliore efficacement les performances tout en considérant les avantages de Python tels que la flexibilité et la lisibilité du code, et l'utilisation d'excellentes bibliothèques telles que Numpy. J'ai senti qu'il était bon d'identifier les endroits où cela serait possible.
Recommended Posts