Dans mon code python
, j'ai besoin de boucler plus de 25 millions de fois, ce que je souhaite optimiser autant que possible. Les opérations dans la boucle sont très simples. Afin de rendre le code efficace, j'ai utilisé le module numba
, qui aide énormément, mais si possible, je voudrais optimiser davantage le code.
Voici un exemple de travail complet: p >
import numba as nb import numpy as np import time #######create some synthetic data for illustration purpose################## size=5000 eps = 0.2 theta_c = 0.4 temp = np.ones(size) neighbour = np.random.randint(size, size=(size, 3)) coschi = np.random.random_sample((size)) theta = np.random.random_sample((size))*np.pi/2 pwr = np.cos(theta) ###################end of dummy data########################## ###################-----main loop------############### @nb.jit(fastmath=True) def func(theta, pwr, neighbour, coschi, temp): for k in range(np.argmax(pwr), 5000*(pwr.size)): n = k%pwr.size if (np.abs(theta[n]-np.pi/2.)<np.abs(theta_c)): adj = neighbour[n,1] else: adj = neighbour[n,0] psi_diff = np.abs(np.arccos(coschi[adj])-np.arccos(coschi[n])) temp5 = temp[adj]**5; e_temp = 1.- np.exp(-temp5*psi_diff/np.abs(eps)) temp[n] = temp[adj] + (e_temp)/temp5*(pwr[n] - temp[adj]**4) return temp #check time time1 = time.time() temp = func(theta, pwr, neighbour, coschi, temp) print("Took: ", time.time()-time1, " seconds.")
Cela prend 3,49 secondes
sur ma machine.
J'ai besoin d'exécuter ce code plusieurs milliers de fois pour un ajustement de modèle, et donc l'optimisation de ne serait-ce que 1 seconde me permet de gagner des dizaines d'heures.
Que peut-on faire pour optimiser davantage ce code?
3 Réponses :
Il semble que vous traitez de nombreux doublons dans votre exemple.
Dans cette version, je ne recalcule aucune valeur pour un «n» que nous avons déjà vu.
Je ne sais pas si c'est correct dans votre cas ou non, mais cela me fait gagner environ 0,4 seconde.
#!/usr/bin/env python import numba as nb import numpy as np import time temp = np.ones(25000000) @nb.jit(fastmath=True) def func(temp): return [n for n in temp] time1 = time.time() result = func(temp) print("Took: ", time.time()-time1, " seconds for ", len(temp), " items")
Original: Hashtable / p> Une boucle simple de 25 millions d'éléments utilisant np.ones: A pris: 1,2502222061157227 secondes pour 25000000 éléments p > A pris: 1.294729232788086 secondes pour 25000000 éléments A pris: 1.2670648097991943 secondes pour 25000000 éléments A pris: 1.2386720180511475 secondes pour 25000000 éléments A pris: 1.2517566680908203 secondes pour 25000000 articles #!/usr/bin/env python
import numba as nb
import numpy as np
import time
#######create some synthetic data for illustration purpose##################
size = 5000
eps = 0.2
theta_c = 0.4
temp = np.ones(size)
neighbour = np.random.randint(size, size=(size, 3))
coschi = np.random.random_sample((size))
theta = np.random.random_sample((size))*np.pi/2
pwr = np.cos(theta)
###################end of dummy data##########################
###################-----main loop------###############
@nb.jit(fastmath=True)
def func(theta, pwr, neighbour, coschi, temp):
hashtable = {}
for k in range(np.argmax(pwr), 5000*(pwr.size)):
n = k % pwr.size
if not hashtable.get(n, False):
hashtable[n] = 1
#taking into account regions with different super wind direction
if (np.abs(theta[n]-np.pi/2.) < np.abs(theta_c)):
adj = neighbour[n, 1]
else:
adj = neighbour[n, 0]
psi_diff = np.abs(np.arccos(coschi[adj])-np.arccos(coschi[n]))
temp5 = temp[adj]**5
e_temp = 1. - np.exp(-temp5*psi_diff/np.abs(eps))
retval = temp[adj] + (e_temp)/temp5*(pwr[n] - temp[adj]**4)
temp[n] = retval
return temp
#check time
time1 = time.time()
result = func(theta, pwr, neighbour, coschi, temp)
print("Took: ", time.time()-time1, "
J'ai juste eu une autre idée. Si ce problème peut être divisé (et il semble que cela soit possible), vous pourrez peut-être utiliser des threads et / ou la concurrence pour calculer simultanément des sous-ensembles de l'espace et les recombiner à la fin. Je n'ai pas le temps de l'essayer maintenant, mais ce sera un exercice intéressant.
Je pense que le calcul répété des valeurs temp [n]
est prévu, sinon il ne serait pas logique de boucler jusqu'à 5000 * pwr.size
, on pourrait s'arrêter beaucoup beaucoup) plus tôt, à pwr.size + np.argmax (pwr)
pour être précis. Ce calcul croisé multiple rend également le problème très difficile à paralléliser car il faut calculer des sous-ensembles d'indices qui peuvent s'influencer les uns les autres et ne pas les calculer en parallèle.
Certes, je ne comprends pas tout à fait ce que fait cette fumction, mais comme nous écrasons parfois temp [n] avant de l'utiliser à nouveau, cela semble être un gaspillage d'efforts. L'espace du problème est peut-être trop grand, mais même quelque chose comme des tables arc-en-ciel pourrait aider ici.
Je suppose que c'est une fonction d'adaptation itérative, il est donc logique de remplacer le temp [n]
en fonction du nombre d'itérations. Je ne comprends pas non plus l'algorithme (ou le raisonnement sous-jacent), mais l'approche de votre réponse modifie considérablement le résultat. Je voulais juste vous en informer - au cas où vous ne l'auriez pas remarqué.
C'était censé être une méthode itérative comme l'a souligné @MSeifert.
Numba est vraiment génial. Mais vous êtes désespéré, n'oubliez pas que vous pouvez toujours écrire en C (youtube). Sur mon propre problème, j'ai obtenu un gain de performance de 30% par rapport à numba simplement en traduisant ligne par ligne en C.
Si vous souhaitez dépenser cet effort, je vous suggère d'utiliser eigen pour les opérations vectorielles (avec une taille de vecteur connue au moment de la compilation) et pybind11 car il se traduit de manière native entre numpy et eigen. Gardez votre boucle principale en Python, bien sûr. Assurez-vous d'utiliser les indicateurs de compilation appropriés (comme -O3
-march = native
, -mtune = native
, -ffast-math
) et essayez différents compilateurs (pour moi, la sortie de gcc
était deux fois plus rapide que celle de clang
, mais des collègues ont signalé le contraire).
Si vous ne connaissez aucun C ++, il peut être plus judicieux de vous limiter au C pur et à aucune bibliothèque (car cela réduit la complexité). Mais vous allez traiter directement les API Python et numpy C (pas si compliqué, mais beaucoup plus de code, et vous apprendrez tout sur les composants internes de Python).
Permettez-moi de commencer par quelques commentaires généraux:
Si vous utilisez numba et que vous vous souciez vraiment des performances, vous devez éviter toute possibilité que numba crée du code en mode objet. Cela signifie que vous devez utiliser numba.njit (...)
ou numba.jit (nopython = True, ...)
au lieu de numba.jit (. ..)
.
Cela ne fait aucune différence dans votre cas, mais cela rend l'intention plus claire et lance des exceptions dès que quelque chose n'est pas pris en charge en mode nopython (rapide).
Vous devez faire attention à ce que vous chronométrez et comment. Le premier appel à une fonction numba-jitted (qui n'est pas compilée à l'avance) inclura le coût de compilation. Vous devez donc l'exécuter une fois avant de le chronométrer pour obtenir des horaires précis. Et pour des horaires plus précis, vous devez appeler la fonction plus d'une fois. J'aime IPythons % timeit
dans Jupyter Notebooks / Lab pour avoir une idée approximative des performances.
Je vais donc utiliser:
499 ms ± 4.47 ms per loop (mean ± std. dev. of 7 runs, 1 loop each) # func4 with fastmath on func4_inner
De cette façon, j'utilise le premier appel (qui inclut le temps de compilation) avec une assertion pour m'assurer qu'il produit vraiment (presque) la même sortie, puis chronométrer la fonction en utilisant une méthode de synchronisation plus robuste (par rapport au temps
).
np.arccos
Commençons maintenant par quelques optimisations de performances réelles: une évidence est que vous pouvez hisser certains "invariants", par exemple le np.arccos (coschi [...])
est calculé beaucoup plus souvent qu'il n'y a d'éléments réels dans le coschi
. Vous parcourez chaque élément de coschi
environ 5000 fois et cela fait deux np.arccos
par boucle! Calculons donc une fois le arccos
du coschi
et stockons-le dans un tableau intermédiaire pour pouvoir y accéder à l'intérieur de la boucle:
1.79 s ± 49.1 ms per loop (mean ± std. dev. of 7 runs, 1 loop each) # original 844 ms ± 41.4 ms per loop (mean ± std. dev. of 7 runs, 1 loop each) # func2 707 ms ± 31.8 ms per loop (mean ± std. dev. of 7 runs, 1 loop each) # func3 550 ms ± 4.88 ms per loop (mean ± std. dev. of 7 runs, 1 loop each) # func4
np.tile
et en découpant au lieu de l'approche range
avec %
. np.arccos
. @nb.njit(fastmath=True) def func2(theta, pwr, neighbour, coschi, temp): arccos_coschi = np.arccos(coschi) for k in range(np.argmax(pwr), 5000 * pwr.size): n = k % pwr.size if np.abs(theta[n] - np.pi / 2.) < np.abs(theta_c): adj = neighbour[n, 1] else: adj = neighbour[n, 0] psi_diff = np.abs(arccos_coschi[adj] - arccos_coschi[n]) temp5 = temp[adj]**5; e_temp = 1. - np.exp(-temp5 * psi_diff / np.abs(eps)) temp[n] = temp[adj] + e_temp / temp5 * (pwr[n] - temp[adj]**4) return temp
Donc, au final, la dernière approche est environ 3 fois plus rapide (sans fastmath
) que l'approche originale. Si vous êtes sûr de vouloir utiliser fastmath
, appliquez simplement fastmath = True
sur le func4_inner
et ce sera encore plus rapide:
res1 = func(theta, pwr, neighbour, coschi, np.ones(size)) res2 = # other approach np.testing.assert_allclose(res1, res2) %timeit func(theta, pwr, neighbour, coschi, np.ones(size)) %timeit # other approach
Cependant, comme je l'ai déjà dit, fastmath
peut ne pas être approprié si vous voulez des résultats exacts (ou du moins pas trop inexacts).
Ici aussi, plusieurs optimisations dépendent beaucoup du matériel disponible et des caches de processeur (en particulier pour les parties du code limitées en bande passante mémoire). Vous devez vérifier comment ces approches fonctionnent les unes par rapport aux autres sur votre ordinateur.
Sur ma machine, l'affectation à temp [n] prend environ 75% du temps. Si je supprime l'affectation avant la température de retour, la durée d'exécution est d'environ 0,55 seconde. Si vous pouvez trouver une meilleure structure de données pour temp, vous pourrez peut-être l'améliorer.
En dehors des autres réponses jusqu'à présent 1) Assurez-vous que Intel SVML est installé (accélération significative des fonctions comme cos, arccos, ...). numba.pydata.org/numba-doc/latest/user/ performance-tips.html 2) Vous pouvez également définir error_model = "numpy" pour éviter la division coûteuse en vérifiant zéro. 3) Vous mesurez un mélange de compilation et d'exécution, exécutez la fonction une fois avant le chronométrage.