52 votes

Optimisation du code d'accès au dictionnaire Python

Question :

J'ai profilé mon programme Python à mort, et il y a une fonction qui ralentit tout. Elle utilise beaucoup les dictionnaires Python, et je ne les ai peut-être pas utilisés de la meilleure façon. Si je ne parviens pas à l'accélérer, je devrai la réécrire en C++. Y a-t-il quelqu'un qui puisse m'aider à l'optimiser en Python ?

J'espère que j'ai donné les bonnes explications et que mon code a un sens pour vous ! Merci d'avance pour toute aide.

Mon code :

Voici la fonction incriminée, profilée à l'aide de line_profiler et kernprof . J'utilise Python 2.7.

Je suis particulièrement intrigué par des choses comme les lignes 363, 389 et 405, où une if avec une comparaison de deux variables semble prendre un temps démesuré.

J'ai envisagé d'utiliser NumPy (comme il le fait pour les matrices éparses) mais je ne pense pas que ce soit approprié car : (1) je n'indexe pas ma matrice en utilisant des entiers (j'utilise des instances d'objets) ; et (2) je ne stocke pas des types de données simples dans la matrice (je stocke des tuples d'un float et d'une instance d'objet). Mais je suis prêt à me laisser convaincre par NumPy. Si quelqu'un connaît les performances des matrices éparses de NumPy par rapport aux tables de hachage de Python, je serais intéressé.

Désolé de ne pas avoir donné un exemple simple que vous pouvez exécuter, mais cette fonction est liée à un projet beaucoup plus vaste et je n'ai pas pu trouver comment mettre en place un exemple simple pour la tester, sans vous donner la moitié de ma base de code !

Timer unit: 3.33366e-10 s
File: routing_distances.py
Function: propagate_distances_node at line 328
Total time: 807.234 s

Line #   Hits         Time  Per Hit   % Time  Line Contents
328                                               @profile
329                                               def propagate_distances_node(self, node_a, cutoff_distance=200):
330                                                       
331                                                   # a makes sure its immediate neighbours are correctly in its distance table
332                                                   # because its immediate neighbours may change as binds/folding change
333    737753   3733642341   5060.8      0.2          for (node_b, neighbour_distance_b_a) in self.neighbours[node_a].iteritems():
334    512120   2077788924   4057.2      0.1              use_neighbour_link = False
335                                                       
336    512120   2465798454   4814.9      0.1              if(node_b not in self.node_distances[node_a]): # a doesn't know distance to b
337     15857     66075687   4167.0      0.0                  use_neighbour_link = True
338                                                       else: # a does know distance to b
339    496263   2390534838   4817.1      0.1                  (node_distance_b_a, next_node) = self.node_distances[node_a][node_b]
340    496263   2058112872   4147.2      0.1                  if(node_distance_b_a > neighbour_distance_b_a): # neighbour distance is shorter
341        81       331794   4096.2      0.0                      use_neighbour_link = True
342    496182   2665644192   5372.3      0.1                  elif((None == next_node) and (float('+inf') == neighbour_distance_b_a)): # direct route that has just broken
343        75       313623   4181.6      0.0                      use_neighbour_link = True
344                                                               
345    512120   1992514932   3890.7      0.1              if(use_neighbour_link):
346     16013     78149007   4880.3      0.0                  self.node_distances[node_a][node_b] = (neighbour_distance_b_a, None)
347     16013     83489949   5213.9      0.0                  self.nodes_changed.add(node_a)
348                                                           
349                                                           ## Affinity distances update
350     16013     86020794   5371.9      0.0                  if((node_a.type == Atom.BINDING_SITE) and (node_b.type == Atom.BINDING_SITE)):
351       164      3950487  24088.3      0.0                      self.add_affinityDistance(node_a, node_b, self.chemistry.affinity(node_a.data, node_b.data))     
352                                                   
353                                                   # a sends its table to all its immediate neighbours
354    737753   3549685140   4811.5      0.1          for (node_b, neighbour_distance_b_a) in self.neighbours[node_a].iteritems():
355    512120   2129343210   4157.9      0.1              node_b_changed = False
356                                               
357                                                       # b integrates a's distance table with its own
358    512120   2203821081   4303.3      0.1              node_b_chemical = node_b.chemical
359    512120   2409257898   4704.5      0.1              node_b_distances = node_b_chemical.node_distances[node_b]
360                                                       
361                                                       # For all b's routes (to c) that go to a first, update their distances
362  41756882 183992040153   4406.3      7.6              for node_c, (distance_b_c, node_after_b) in node_b_distances.iteritems(): # Think it's ok to modify items while iterating over them (just not insert/delete) (seems to work ok)
363  41244762 172425596985   4180.5      7.1                  if(node_after_b == node_a):
364                                                               
365  16673654  64255631616   3853.7      2.7                      try:
366  16673654  88781802534   5324.7      3.7                          distance_b_a_c = neighbour_distance_b_a + self.node_distances[node_a][node_c][0]
367    187083    929898684   4970.5      0.0                      except KeyError:
368    187083   1056787479   5648.8      0.0                          distance_b_a_c = float('+inf')
369                                                                   
370  16673654  69374705256   4160.7      2.9                      if(distance_b_c != distance_b_a_c): # a's distance to c has changed
371    710083   3136751361   4417.4      0.1                          node_b_distances[node_c] = (distance_b_a_c, node_a)
372    710083   2848845276   4012.0      0.1                          node_b_changed = True
373                                                                   
374                                                                   ## Affinity distances update
375    710083   3484577241   4907.3      0.1                          if((node_b.type == Atom.BINDING_SITE) and (node_c.type == Atom.BINDING_SITE)):
376     99592   1591029009  15975.5      0.1                              node_b_chemical.add_affinityDistance(node_b, node_c, self.chemistry.affinity(node_b.data, node_c.data))
377                                                                   
378                                                               # If distance got longer, then ask b's neighbours to update
379                                                               ## TODO: document this!
380  16673654  70998570837   4258.1      2.9                      if(distance_b_a_c > distance_b_c):
381                                                                   #for (node, neighbour_distance) in node_b_chemical.neighbours[node_b].iteritems():
382   1702852   7413182064   4353.4      0.3                          for node in node_b_chemical.neighbours[node_b]:
383   1204903   5912053272   4906.7      0.2                              node.chemical.nodes_changed.add(node)
384                                                       
385                                                       # Look for routes from a to c that are quicker than ones b knows already
386  42076729 184216680432   4378.1      7.6              for node_c, (distance_a_c, node_after_a) in self.node_distances[node_a].iteritems():
387                                                           
388  41564609 171150289218   4117.7      7.1                  node_b_update = False
389  41564609 172040284089   4139.1      7.1                  if(node_c == node_b): # a-b path
390    512120   2040112548   3983.7      0.1                      pass
391  41052489 169406668962   4126.6      7.0                  elif(node_after_a == node_b): # a-b-a-b path
392  16251407  63918804600   3933.1      2.6                      pass
393  24801082 101577038778   4095.7      4.2                  elif(node_c in node_b_distances): # b can already get to c
394  24004846 103404357180   4307.6      4.3                      (distance_b_c, node_after_b) = node_b_distances[node_c]
395  24004846 102717271836   4279.0      4.2                      if(node_after_b != node_a): # b doesn't already go to a first
396   7518275  31858204500   4237.4      1.3                          distance_b_a_c = neighbour_distance_b_a + distance_a_c
397   7518275  33470022717   4451.8      1.4                          if(distance_b_a_c < distance_b_c): # quicker to go via a
398    225357    956440656   4244.1      0.0                              node_b_update = True
399                                                           else: # b can't already get to c
400    796236   3415455549   4289.5      0.1                      distance_b_a_c = neighbour_distance_b_a + distance_a_c
401    796236   3412145520   4285.3      0.1                      if(distance_b_a_c < cutoff_distance): # not too for to go
402    593352   2514800052   4238.3      0.1                          node_b_update = True
403                                                                   
404                                                           ## Affinity distances update
405  41564609 164585250189   3959.7      6.8                  if node_b_update:
406    818709   3933555120   4804.6      0.2                      node_b_distances[node_c] = (distance_b_a_c, node_a)
407    818709   4151464335   5070.7      0.2                      if((node_b.type == Atom.BINDING_SITE) and (node_c.type == Atom.BINDING_SITE)):
408    104293   1704446289  16342.9      0.1                          node_b_chemical.add_affinityDistance(node_b, node_c, self.chemistry.affinity(node_b.data, node_c.data))
409    818709   3557529531   4345.3      0.1                      node_b_changed = True
410                                                       
411                                                       # If any of node b's rows have exceeded the cutoff distance, then remove them
412  42350234 197075504439   4653.5      8.1              for node_c, (distance_b_c, node_after_b) in node_b_distances.items(): # Can't use iteritems() here, as deleting from the dictionary
413  41838114 180297579789   4309.4      7.4                  if(distance_b_c > cutoff_distance):
414    206296    894881754   4337.9      0.0                      del node_b_distances[node_c]
415    206296    860508045   4171.2      0.0                      node_b_changed = True
416                                                               
417                                                               ## Affinity distances update
418    206296   4698692217  22776.5      0.2                      node_b_chemical.del_affinityDistance(node_b, node_c)
419                                                       
420                                                       # If we've modified node_b's distance table, tell its chemical to update accordingly
421    512120   2130466347   4160.1      0.1              if(node_b_changed):
422    217858   1201064454   5513.1      0.0                  node_b_chemical.nodes_changed.add(node_b)
423                                                   
424                                                   # Remove any neighbours that have infinite distance (have just unbound)
425                                                   ## TODO: not sure what difference it makes to do this here rather than above (after updating self.node_distances for neighbours)
426                                                   ##       but doing it above seems to break the walker's movement
427    737753   3830386968   5192.0      0.2          for (node_b, neighbour_distance_b_a) in self.neighbours[node_a].items(): # Can't use iteritems() here, as deleting from the dictionary
428    512120   2249770068   4393.1      0.1              if(neighbour_distance_b_a > cutoff_distance):
429       150       747747   4985.0      0.0                  del self.neighbours[node_a][node_b]
430                                                           
431                                                           ## Affinity distances update
432       150      2148813  14325.4      0.0                  self.del_affinityDistance(node_a, node_b)

Explication de mon code :

Cette fonction maintient une matrice de distance éparse représentant la distance du réseau (somme des poids des arêtes sur le chemin le plus court) entre les nœuds d'un (très grand) réseau. Pour travailler avec le tableau complet et utiliser la fonction Algorithme de Floyd-Warshall serait très lent (j'ai d'abord essayé cela, et c'était des ordres de grandeur plus lent que la version actuelle). Mon code utilise donc une matrice éparse pour représenter une version seuillée de la matrice de distance complète (tous les chemins dont la distance est supérieure à 200 unités sont ignorés). La topologie du réseau change au fil du temps, donc cette matrice de distance doit être mise à jour au fil du temps. Pour ce faire, j'utilise une implémentation grossière d'une matrice de distance de type protocole de routage distance-vecteur : chaque nœud du réseau connaît la distance qui le sépare des autres nœuds et du prochain nœud du chemin. Lorsqu'un changement de topologie se produit, le ou les nœuds associés à ce changement mettent à jour leur(s) table(s) de distance en conséquence, et en informent leurs voisins immédiats. L'information est diffusée dans le réseau par les nœuds qui envoient leurs tables de distance à leurs voisins, qui mettent à jour leurs tables de distance et les diffusent à leurs voisins.

Il existe un objet représentant la matrice de distance : self.node_distances . Il s'agit d'un dictionnaire faisant correspondre les nœuds aux tables de routage. Un nœud est un objet que j'ai défini. Une table de routage est un dictionnaire faisant correspondre des noeuds à des tuples de (distance, next_node). Distance est la distance graphique entre le noeud_a et le noeud_b, et next_node est le voisin du noeud_a que vous devez visiter en premier, sur le chemin entre le noeud_a et le noeud_b. Un next_node de None indique que les nœuds_a et node_b sont des voisins du graphe. Par exemple, un exemple de matrice de distance pourrait être :

self.node_distances = { node_1 : { node_2 : (2.0, None),
                                   node_3 : (5.7, node_2),
                                   node_5 : (22.9, node_2) },
                        node_2 : { node_1 : (2.0, None),
                                   node_3 : (3.7, None),
                                   node_5 : (20.9, node_7)},
                        ...etc...

En raison des changements de topologie, deux nœuds qui étaient éloignés (ou pas du tout connectés) peuvent devenir proches. Lorsque cela se produit, des entrées sont ajoutées à cette matrice. En raison du seuillage, deux nœuds peuvent devenir trop éloignés l'un de l'autre pour être pris en compte. Dans ce cas, des entrées sont supprimées de cette matrice.

El self.neighbours est similaire à self.node_distances mais contient des informations sur les liens directs (arêtes) du réseau. self.neighbours est continuellement modifié à l'extérieur de cette fonction, par la réaction chimique. C'est de là que proviennent les modifications de la topologie du réseau.

La fonction réelle avec laquelle j'ai des problèmes : propagate_distances_node() exécute une étape de la protocole de routage distance-vecteur . Étant donné un nœud, node_a la fonction s'assure que node_a Les voisins de l'utilisateur sont correctement inscrits dans la matrice de distance (la topologie change). La fonction envoie ensuite node_a de la table de routage de l'entreprise vers tous les node_a dans le réseau. Il intègre node_a avec la table de routage de chaque voisin.

Dans le reste de mon programme, le propagate_distances_node() est appelée à plusieurs reprises, jusqu'à ce que la matrice de distance converge. Un ensemble, self.nodes_changed est maintenue, des noeuds qui ont modifié leur table de routage depuis la dernière mise à jour. À chaque itération de mon algorithme, un sous-ensemble aléatoire de ces nœuds est choisi et propagate_distances_node() est appelé sur eux. Cela signifie que les nœuds diffusent leurs tables de routage de manière asynchrone et stochastique. Cet algorithme converge vers la vraie matrice de distance lorsque l'ensemble self.nodes_changed devient vide.

Les parties "distances d'affinité" ( add_affinityDistance y del_affinityDistance ) sont un cache d'une (petite) sous-matrice de la matrice de distance, qui est utilisée par une autre partie du programme.

La raison pour laquelle je fais cela est que je simule des analogues informatiques de produits chimiques participant à des réactions, dans le cadre de mon doctorat. Un "produit chimique" est un graphe d'"atomes" (nœuds du graphe). La liaison entre deux produits chimiques est simulée par la jonction de leurs deux graphes par de nouvelles arêtes. Une réaction chimique se produit (par un processus compliqué qui n'est pas pertinent ici), modifiant la topologie du graphe. Mais ce qui se passe dans la réaction dépend de la distance qui sépare les différents atomes qui composent les produits chimiques. Ainsi, pour chaque atome de la simulation, je veux savoir de quels autres atomes il est proche. Une matrice de distance éparse et seuillée est le moyen le plus efficace de stocker ces informations. Comme la topologie du réseau change au fur et à mesure que la réaction se produit, je dois mettre à jour la matrice. A protocole de routage distance-vecteur est le moyen le plus rapide que j'ai trouvé pour faire cela. Je n'ai pas besoin d'un protocole de routage plus compliqué, car des choses comme les boucles de routage ne se produisent pas dans mon application particulière (en raison de la façon dont mes produits chimiques sont structurés). La raison pour laquelle je le fais de manière stochastique est que je peux imbriquer les processus de réaction chimique avec l'étalement de la distance, et simuler un produit chimique changeant progressivement de forme au fil du temps au fur et à mesure que la réaction se produit (plutôt que de changer de forme instantanément).

El self dans cette fonction est un objet représentant un produit chimique. Les nœuds dans self.node_distances.keys() sont les atomes qui composent le produit chimique. Les nœuds dans self.node_distances[node_x].keys() sont des nœuds du produit chimique et potentiellement des nœuds de tout produit chimique auquel le produit chimique est lié (et avec lequel il réagit).

Mise à jour :

J'ai essayé de remplacer chaque instance de node_x == node_y con node_x is node_y (conformément au commentaire de @Sven Marnach), mais ça a ralenti les choses ! (Je ne m'y attendais pas !) Mon profil original prenait 807.234s pour s'exécuter, mais avec cette modification il est passé à 895.895s. Désolé, je faisais mal le profilage ! J'utilisais line_by_line, qui (sur mon code) avait beaucoup trop de variance (cette différence de ~90 secondes était dans le bruit). En le profilant correctement, is est infiniment plus rapide que == . Utilisation de CProfile mon code avec == a pris 34.394s, mais avec is il a fallu 33.535s (ce que je peux confirmer est hors du bruit).

Mise à jour : Bibliothèques existantes

Je ne suis pas sûr qu'il existe une bibliothèque existante capable de faire ce que je veux, car mes exigences sont inhabituelles : J'ai besoin de calculer les longueurs des plus courts chemins entre toutes les paires de nœuds dans un graphe pondéré et non dirigé. Je ne me préoccupe que des longueurs de chemin qui sont inférieures à une valeur seuil. Après avoir calculé les longueurs de chemin, j'apporte une petite modification à la topologie du réseau (ajout ou suppression d'une arête), puis je veux recalculer les longueurs de chemin. Mes graphes sont énormes par rapport à la valeur seuil (à partir d'un nœud donné, la plupart du graphe est plus éloigné que le seuil), et donc les changements de topologie n'affectent pas la plupart des longueurs de chemin les plus courts. C'est la raison pour laquelle j'utilise l'algorithme de routage : parce qu'il diffuse l'information sur les changements de topologie à travers la structure du graphe, et que je peux arrêter de la diffuser lorsqu'elle a dépassé le seuil. Je peux utiliser les informations précédentes sur les chemins (d'avant le changement de topologie) pour accélérer le calcul. C'est pourquoi je pense que mon algorithme sera plus rapide que toutes les implémentations de bibliothèques d'algorithmes de plus court chemin. Je n'ai jamais vu d'algorithmes de routage utilisés en dehors du routage de paquets dans des réseaux physiques (mais si quelqu'un l'a fait, je serais intéressé).

NetworkX a été suggéré par @Thomas K. Il a beaucoup d'algorithmes pour calculer les plus courts chemins. Il dispose d'un algorithme pour calculer le longueurs des plus courts chemins de toutes les paires avec une coupure (ce qui est ce que je veux), mais il ne fonctionne que sur des graphes non pondérés (les miens sont pondérés). Malheureusement, son algorithmes pour les graphes pondérés ne permettent pas l'utilisation d'un seuil (ce qui pourrait les rendre lents pour mes graphiques). Et aucun de ses algorithmes ne semble supporter l'utilisation de chemins précalculés sur un réseau très similaire (c'est-à-dire le routage).

igraphe est une autre bibliothèque de graphiques que je connais, mais en regardant sa documentation Je n'ai rien trouvé sur les chemins les plus courts. Mais je pourrais l'avoir manqué - sa documentation ne semble pas très complète.

NumPy pourrait être possible, grâce au commentaire de @9000. Je peux stocker ma matrice éparse dans un tableau NumPy si j'attribue un entier unique à chaque instance de mes nœuds. Je peux alors indexer un tableau NumPy avec des entiers au lieu d'instances de nœuds. J'aurai également besoin de deux tableaux NumPy : un pour les distances et un pour les références "next_node". Cela pourrait être plus rapide que d'utiliser les dictionnaires Python (je ne sais pas encore).

Quelqu'un connaît-il d'autres bibliothèques qui pourraient être utiles ?

Mise à jour : Utilisation de la mémoire

J'utilise Windows (XP), voici donc quelques informations sur l'utilisation de la mémoire, provenant de Explorateur de processus . L'utilisation du CPU est de 50% car j'ai une machine à deux cœurs.

global memory usagemy program's memory usage

Mon programme n'est pas à court de RAM et ne commence pas à frapper le swap. Vous pouvez le voir d'après les chiffres, et d'après le graphique IO qui n'a aucune activité. Les pics sur le graphique IO sont ceux que le programme imprime à l'écran pour dire comment il se comporte.

Cependant, mon programme utilise de plus en plus de RAM au fil du temps, ce qui n'est probablement pas une bonne chose (mais il n'utilise pas beaucoup de RAM dans l'ensemble, c'est pourquoi je n'ai pas remarqué l'augmentation jusqu'à maintenant).

Et la distance entre les pics sur le graphique IO augmente avec le temps. C'est mauvais - mon programme imprime à l'écran toutes les 100 000 itérations, ce qui signifie que chaque itération prend plus de temps à s'exécuter au fil du temps... J'ai confirmé cela en faisant une longue exécution de mon programme et en mesurant le temps entre les instructions d'impression (le temps entre chaque 10 000 itérations du programme). Ce temps devrait être constant, mais comme vous pouvez le voir sur le graphique, il augmente linéairement... il y a donc quelque chose qui se passe. (Le bruit sur ce graphique est dû au fait que mon programme utilise beaucoup de nombres aléatoires, donc le temps pour chaque itération varie).

lag between print statements increasing over time

Après que mon programme ait été exécuté pendant une longue période, l'utilisation de la mémoire ressemble à ceci (donc il ne manque pas de RAM) :

global memory usage - after a long runmy program's memory usage - after a long run

16voto

Paulo Scardine Points 17518

node_after_b == node_a va essayer d'appeler node_after_b.__eq__(node_a) :

>>> class B(object):
...     def __eq__(self, other):
...         print "B.__eq__()"
...         return False
... 
>>> class A(object):
...     def __eq__(self, other):
...         print "A.__eq__()"
...         return False
... 
>>> a = A()
>>> b = B()
>>> a == b
A.__eq__()
False
>>> b == a
B.__eq__()
False
>>> 

Essayez de passer outre Node.__eq__() avec une version optimisée avant de recourir au C.

UPDATE

J'ai fait cette petite expérience (python 2.6.6) :

#!/usr/bin/env python
# test.py
class A(object):
    def __init__(self, id):
        self.id = id

class B(A):
    def __eq__(self, other):
        return self.id == other.id

@profile
def main():
    list_a = []
    list_b = []
    for x in range(100000):
        list_a.append(A(x))
        list_b.append(B(x))

    ob_a = A(1)
    ob_b = B(1)
    for ob in list_a:
        if ob == ob_a:
            x = True
        if ob is ob_a:
            x = True
        if ob.id == ob_a.id:
            x = True
        if ob.id == 1:
            x = True
    for ob in list_b:
        if ob == ob_b:
            x = True
        if ob is ob_b:
            x = True
        if ob.id == ob_b.id:
            x = True
        if ob.id == 1:
            x = True

if __name__ == '__main__':
    main()

Résultats :

Timer unit: 1e-06 s

File: test.py Function: main at line 10 Total time: 5.52964 s

Line #      Hits         Time  Per Hit % Time  Line Contents
==============================================================
    10                                           @profile
    11                                           def main():
    12         1            5      5.0      0.0      list_a = []
    13         1            3      3.0      0.0      list_b = []
    14    100001       360677      3.6      6.5      for x in range(100000):
    15    100000       763593      7.6     13.8          list_a.append(A(x))
    16    100000       924822      9.2     16.7          list_b.append(B(x))
    17
    18         1           14     14.0      0.0      ob_a = A(1)
    19         1            5      5.0      0.0      ob_b = B(1)
    20    100001       500454      5.0      9.1      for ob in list_a:
    21    100000       267252      2.7      4.8          if ob == ob_a:
    22                                                       x = True
    23    100000       259075      2.6      4.7          if ob is ob_a:
    24                                                       x = True
    25    100000       539683      5.4      9.8          if ob.id == ob_a.id:
    26         1            3      3.0      0.0              x = True
    27    100000       271519      2.7      4.9          if ob.id == 1:
    28         1            3      3.0      0.0              x = True
    29    100001       296736      3.0      5.4      for ob in list_b:
    30    100000       472204      4.7      8.5          if ob == ob_b:
    31         1            4      4.0      0.0              x = True
    32    100000       283165      2.8      5.1          if ob is ob_b:
    33                                                       x = True
    34    100000       298839      3.0      5.4          if ob.id == ob_b.id:
    35         1            3      3.0      0.0              x = True
    36    100000       291576      2.9      5.3          if ob.id == 1:
    37         1            3      3.0      0.0              x = True

J'ai été très surpris :

  • L'accès "point" (ob.property) semble être très coûteux (ligne 25 contre ligne 27).
  • il n'y avait pas beaucoup de différence entre is et '==', au moins pour les objets simples

J'ai ensuite essayé avec des objets plus complexes et les résultats sont cohérents avec la première expérience.

Faites-vous beaucoup d'échanges ? Si votre ensemble de données est si grand qu'il ne tient pas dans la RAM disponible, je pense que vous rencontrez une sorte de conflit d'E/S lié à la recherche de mémoire virtuelle.

Utilisez-vous Linux ? Si oui, pourriez-vous afficher un vmstat de votre machine pendant l'exécution de votre programme ? Envoyez-nous la sortie de quelque chose comme :

vmstat 10 100

Bonne chance !

MISE À JOUR (à partir des commentaires du PO)

J'ai suggéré de jouer avec sys.setcheckinterval et d'activer/désactiver le GC. Le raisonnement est que pour ce cas particulier (un grand nombre d'instances), la vérification par défaut du nombre de références du GC est quelque peu coûteuse et son intervalle par défaut est trop fréquent.

Oui, j'avais déjà joué avec sys.setcheckinterval. Je l'ai changé en 1000 (au lieu de sa valeur par défaut de 100), mais cela mais cela n'a pas fait de différence mesurable. La désactivation de Garbage Collection a a aidé - merci. Cela a été la le plus gros gain de vitesse jusqu'à présent - environ 20% (171 minutes pour l'ensemble de l'exécution, à 135 minutes) - Je ne suis pas sûr de savoir Je ne suis pas sûr des barres d'erreur, mais mais cela doit être une augmentation statistiquement significative augmentation. - Adam Nellis Fév 9 à 15:10

A mon avis :

Je pense que la GC de Python est basée sur le nombre de références. De temps en temps, il vérifiera le nombre de références pour chaque instance ; puisque vous traversez ces énormes structures en mémoire en mémoire, dans votre cas particulier la fréquence par défaut du GC (1000 cycles ?) est trop fréquente - un énorme gaspillage. gaspillage. - Yours Truly Feb 10 at 2:06

6voto

Macke Points 13474

Avez-vous envisagé Pyrex / Cython ?

Il compile automatiquement python en C puis en .pyd, ce qui peut accélérer les choses sans trop de travail.

4voto

Cela nécessiterait une bonne dose de travail, mais... vous pourriez envisager d'utiliser Floyd-Warshall sur un GPU. Il y a eu beaucoup de travail pour faire tourner Floyd-Warshall très efficacement sur un GPU. Une recherche rapide sur Google donne les résultats suivants :

http://cvit.iiit.ac.in/papers/Pawan07accelerating.pdf

http://my.safaribooksonline.com/book/programming/graphics/9780321545411/gpu-computing-for-protein-structure-prediction/ch43lev1sec2#X2ludGVybmFsX0ZsYXNoUmVhZGVyP3htbGlkPTk3ODAzMjE1NDU0MTEvNDg3

http://www.gpucomputing.net/?q=node/1203

http://http.developer.nvidia.com/GPUGems2/gpugems2_chapter43.html

Même si, tel qu'implémenté en Python, Floyd-Warshall était plus lent d'un ordre de grandeur, une bonne version GPU sur un GPU puissant pourrait encore surpasser de manière significative votre nouveau code Python.

Voici une anecdote. J'avais un morceau de code court, simple et gourmand en ressources informatiques qui faisait quelque chose de similaire à une accumulation de Hough. En Python, optimisé au maximum, il prenait ~7s sur un i7 rapide. J'ai ensuite écrit une version GPU complètement non-optimisée ; cela a pris ~0.002s sur une Nvidia GTX 480. YMMV, mais pour tout ce qui est significativement parallèle, le GPU est susceptible d'être un gagnant à long terme, et comme il s'agit d'un algorithme bien étudié, vous devriez être en mesure d'utiliser un code existant très bien réglé.

Pour le pont Python / GPU, je recommande PyCUDA ou PyOpenCL.

3voto

Nas Banov Points 7293

Je ne vois rien d'anormal dans votre code en ce qui concerne les performances (sans essayer de comprendre l'algorithme), vous êtes juste frappé par le grand nombre d'itérations. Des parties de votre code sont exécutées 40 million temps !

Remarquez que 80% du temps est passé dans 20% de votre code - et ce sont les 13 lignes qui sont exécutées 24+ millions de fois. D'ailleurs, avec ce code, vous illustrez parfaitement le concept de Le principe de Pareto (ou "20% des buveurs de bière boivent 80% de la bière").

Commençons par le commencement : avez-vous essayé Psycho ? Il s'agit d'un compilateur JIT qui peut accélérer considérablement votre code - compte tenu du grand nombre d'itérations - par un facteur de 4x-5x - et tout ce que vous avez à faire (après avoir téléchargé et installé, bien sûr) est d'insérer ce bout de code au début :

import psyco
psyco.full()

C'est pourquoi j'ai aimé Psycho et l'ai utilisé dans GCJ aussi, où le temps est essentiel - rien à coder, rien à se tromper et un coup de pouce soudain de 2 lignes ajoutées.

Revenons au pinaillage (qui change comme le fait de remplacer == con is etc., en raison du faible pourcentage de gain de temps). Voici les 13 lignes "fautives" :

Line    #   Hits    Time    Per Hit % Time  Line Contents
412 42350234    197075504439    4653.5  8.1 for node_c, (distance_b_c, node_after_b) in node_b_distances.items(): # Can't use iteritems() here, as deleting from the dictionary
386 42076729    184216680432    4378.1  7.6 for node_c, (distance_a_c, node_after_a) in self.node_distances[node_a].iteritems():
362 41756882    183992040153    4406.3  7.6 for node_c, (distance_b_c, node_after_b) in node_b_distances.iteritems(): # Think it's ok to modify items while iterating over them (just not insert/delete) (seems to work ok)
413 41838114    180297579789    4309.4  7.4 if(distance_b_c > cutoff_distance):
363 41244762    172425596985    4180.5  7.1 if(node_after_b == node_a):
389 41564609    172040284089    4139.1  7.1 if(node_c == node_b): # a-b path
388 41564609    171150289218    4117.7  7.1 node_b_update = False
391 41052489    169406668962    4126.6  7   elif(node_after_a == node_b): # a-b-a-b path
405 41564609    164585250189    3959.7  6.8 if node_b_update:
394 24004846    103404357180    4307.6  4.3 (distance_b_c, node_after_b) = node_b_distances[node_c]
395 24004846    102717271836    4279    4.2 if(node_after_b != node_a): # b doesn't already go to a first
393 24801082    101577038778    4095.7  4.2 elif(node_c in node_b_distances): # b can already get to c

A) En plus des lignes que vous mentionnez, je remarque que la ligne 388 a un temps relativement élevé alors qu'elle est triviale, tout ce qu'elle fait c'est node_b_update = False . Oh mais attendez - chaque fois qu'il est exécuté, False est recherché dans la portée globale ! Pour éviter cela, assignez F, T = False, True au début de la méthode et remplacer les utilisations ultérieures de la False y True avec les habitants F y T . Cela devrait permettre de réduire le temps global, mais de peu (3 % ?).

B) Je remarque que la condition du point 389 ne s'est produite "que" 512 120 fois (sur la base du nombre d'exécutions du point 390), alors que la condition du point 391 s'est produite 16 251 407 fois. Comme il n'y a pas de dépendance, il est logique d'inverser l'ordre de ces vérifications - en raison de la "coupure" précoce qui devrait donner un petit coup de pouce (2 % ?). Je ne suis pas sûr que le fait d'éviter pass L'ensemble des énoncés sera utile, mais si cela ne nuit pas à la lisibilité :

if (node_after_a is not node_b) and (node_c is not node_b):
   # neither a-b-a-b nor a-b path
   if (node_c in node_b_distances): # b can already get to c
       (distance_b_c, node_after_b) = node_b_distances[node_c]
       if (node_after_b is not node_a): # b doesn't already go to a first
           distance_b_a_c = neighbour_distance_b_a + distance_a_c
           if (distance_b_a_c < distance_b_c): # quicker to go via a
               node_b_update = T
   else: # b can't already get to c
       distance_b_a_c = neighbour_distance_b_a + distance_a_c
       if (distance_b_a_c < cutoff_distance): # not too for to go
           node_b_update = T

C) Je viens de remarquer que vous utilisez try-except dans un cas (#365-367) vous avez juste besoin de la valeur par défaut d'un dictionnaire - essayez d'utiliser plutôt .get(key, defaultVal) ou créez vos dictionnaires avec collections.defaultdict(itertools.repeat(float('+inf'))) . L'utilisation de try-except a son prix - voir le rapport #365 qui signale que 3,5% du temps, c'est la mise en place de stack frames et autres.

D) Éviter les accès indexés (que ce soit avec les objets). . champ ou objet [ idx ] ) lorsque cela est possible. Par exemple, je vois que vous utilisez self.node_distances[node_a] à de multiples endroits (#336, 339, 346, 366, 386), ce qui signifie que pour chaque utilisation, l'indexation est utilisée deux fois (une fois pour les . et une fois pour [] ) - et cela devient coûteux lorsqu'il est exécuté des dizaines de millions de fois. Il me semble que vous pouvez simplement faire au début de la méthode node_a_distances = self.node_distances[node_a] et l'utiliser ensuite.

1voto

Adam Nellis Points 607

J'aurais bien posté ceci comme une mise à jour de ma question, mais Stack Overflow ne permet que 30000 caractères dans les questions, donc je poste ceci comme une réponse.

Mise à jour : Mes meilleures optimisations jusqu'à présent

J'ai pris en compte les suggestions des gens, et maintenant mon code s'exécute environ 21% plus vite qu'avant, ce qui est bien - merci à tous !

C'est ce que j'ai réussi à faire de mieux jusqu'à présent. J'ai remplacé tous les == tests avec is pour les nœuds, désactivé le garbage collection et réécrit le grand if partie de l'énoncé à la ligne 388, en accord avec les suggestions de @Nas Banov. J'ai ajouté dans le fameux try/except astuce pour éviter les tests (ligne 390 - pour supprimer le test node_c in node_b_distances ), ce qui a aidé les charges, puisqu'il ne lève presque jamais l'exception. J'ai essayé d'intervertir les lignes 391 et 392, et d'assigner node_b_distances[node_c] à une variable, mais cette façon était la plus rapide.

Cependant, je n'ai toujours pas trouvé la fuite de mémoire (voir le graphique dans ma question). Mais je pense que cela pourrait être dans une autre partie de mon code (que je n'ai pas affichée ici). Si je peux corriger la fuite de mémoire, alors ce programme fonctionnera assez vite pour que je puisse l'utiliser :)

Timer unit: 3.33366e-10 s
File: routing_distances.py
Function: propagate_distances_node at line 328
Total time: 760.74 s

Line #      Hits         Time  Per Hit   % Time  Line Contents
328                                               @profile
329                                               def propagate_distances_node(self, node_a, cutoff_distance=200):
330                                                       
331                                                   # a makes sure its immediate neighbours are correctly in its distance table
332                                                   # because its immediate neighbours may change as binds/folding change
333    791349   4158169713   5254.5      0.2          for (node_b, neighbour_distance_b_a) in self.neighbours[node_a].iteritems():
334    550522   2331886050   4235.8      0.1              use_neighbour_link = False
335                                                       
336    550522   2935995237   5333.1      0.1              if(node_b not in self.node_distances[node_a]): # a doesn't know distance to b
337     15931     68829156   4320.5      0.0                  use_neighbour_link = True
338                                                       else: # a does know distance to b
339    534591   2728134153   5103.2      0.1                  (node_distance_b_a, next_node) = self.node_distances[node_a][node_b]
340    534591   2376374859   4445.2      0.1                  if(node_distance_b_a > neighbour_distance_b_a): # neighbour distance is shorter
341        78       347355   4453.3      0.0                      use_neighbour_link = True
342    534513   3145889079   5885.5      0.1                  elif((None is next_node) and (float('+inf') == neighbour_distance_b_a)): # direct route that has just broken
343        74       327600   4427.0      0.0                      use_neighbour_link = True
344                                                               
345    550522   2414669022   4386.1      0.1              if(use_neighbour_link):
346     16083     81850626   5089.3      0.0                  self.node_distances[node_a][node_b] = (neighbour_distance_b_a, None)
347     16083     87064200   5413.4      0.0                  self.nodes_changed.add(node_a)
348                                                           
349                                                           ## Affinity distances update
350     16083     86580603   5383.4      0.0                  if((node_a.type == Atom.BINDING_SITE) and (node_b.type == Atom.BINDING_SITE)):
351       234      6656868  28448.2      0.0                      self.add_affinityDistance(node_a, node_b, self.chemistry.affinity(node_a.data, node_b.data))     
352                                                   
353                                                   # a sends its table to all its immediate neighbours
354    791349   4034651958   5098.4      0.2          for (node_b, neighbour_distance_b_a) in self.neighbours[node_a].iteritems():
355    550522   2392248546   4345.4      0.1              node_b_changed = False
356                                               
357                                                       # b integrates a's distance table with its own
358    550522   2520330696   4578.1      0.1              node_b_chemical = node_b.chemical
359    550522   2734341975   4966.8      0.1              node_b_distances = node_b_chemical.node_distances[node_b]
360                                                       
361                                                       # For all b's routes (to c) that go to a first, update their distances
362  46679347 222161837193   4759.3      9.7              for node_c, (distance_b_c, node_after_b) in node_b_distances.iteritems(): # Think it's ok to modify items while iterating over them (just not insert/delete) (seems to work ok)
363  46128825 211963639122   4595.0      9.3                  if(node_after_b is node_a):
364                                                               
365  18677439  79225517916   4241.8      3.5                      try:
366  18677439 101527287264   5435.8      4.4                          distance_b_a_c = neighbour_distance_b_a + self.node_distances[node_a][node_c][0]
367    181510    985441680   5429.1      0.0                      except KeyError:
368    181510   1166118921   6424.5      0.1                          distance_b_a_c = float('+inf')
369                                                                   
370  18677439  89626381965   4798.6      3.9                      if(distance_b_c != distance_b_a_c): # a's distance to c has changed
371    692131   3352970709   4844.4      0.1                          node_b_distances[node_c] = (distance_b_a_c, node_a)
372    692131   3066946866   4431.2      0.1                          node_b_changed = True
373                                                                   
374                                                                   ## Affinity distances update
375    692131   3808548270   5502.6      0.2                          if((node_b.type == Atom.BINDING_SITE) and (node_c.type == Atom.BINDING_SITE)):
376     96794   1655818011  17106.6      0.1                              node_b_chemical.add_affinityDistance(node_b, node_c, self.chemistry.affinity(node_b.data, node_c.data))
377                                                                   
378                                                               # If distance got longer, then ask b's neighbours to update
379                                                               ## TODO: document this!
380  18677439  88838493705   4756.5      3.9                      if(distance_b_a_c > distance_b_c):
381                                                                   #for (node, neighbour_distance) in node_b_chemical.neighbours[node_b].iteritems():
382   1656796   7949850642   4798.3      0.3                          for node in node_b_chemical.neighbours[node_b]:
383   1172486   6307264854   5379.4      0.3                              node.chemical.nodes_changed.add(node)
384                                                       
385                                                       # Look for routes from a to c that are quicker than ones b knows already
386  46999631 227198060532   4834.0     10.0              for node_c, (distance_a_c, node_after_a) in self.node_distances[node_a].iteritems():
387                                                           
388  46449109 218024862372   4693.8      9.6                  if((node_after_a is not node_b) and # not a-b-a-b path
389  28049321 126269403795   4501.7      5.5                     (node_c is not node_b)):         # not a-b path
390  27768341 121588366824   4378.7      5.3                      try: # Assume node_c in node_b_distances ('try' block will raise KeyError if not)
391  27768341 159413637753   5740.8      7.0                          if((node_b_distances[node_c][1] is not node_a) and # b doesn't already go to a first
392   8462467  51890478453   6131.8      2.3                             ((neighbour_distance_b_a + distance_a_c) < node_b_distances[node_c][0])):
393                                                               
394                                                                       # Found a route
395    224593   1168129548   5201.1      0.1                              node_b_distances[node_c] = (neighbour_distance_b_a + distance_a_c, node_a)
396                                                                       ## Affinity distances update
397    224593   1274631354   5675.3      0.1                              if((node_b.type == Atom.BINDING_SITE) and (node_c.type == Atom.BINDING_SITE)):
398     32108    551523249  17177.1      0.0                                  node_b_chemical.add_affinityDistance(node_b, node_c, self.chemistry.affinity(node_b.data, node_c.data))
399    224593   1165878108   5191.1      0.1                              node_b_changed = True
400                                                                       
401    809945   4449080808   5493.1      0.2                      except KeyError:
402                                                                   # b can't already get to c (node_c not in node_b_distances)
403    809945   4208032422   5195.5      0.2                          if((neighbour_distance_b_a + distance_a_c) < cutoff_distance): # not too for to go
404                                                                       
405                                                                       # These lines of code copied, for efficiency 
406                                                                       #  (most of the time, the 'try' block succeeds, so don't bother testing for (node_c in node_b_distances))
407                                                                       # Found a route
408    587726   3162939543   5381.7      0.1                              node_b_distances[node_c] = (neighbour_distance_b_a + distance_a_c, node_a)
409                                                                       ## Affinity distances update
410    587726   3363869061   5723.5      0.1                              if((node_b.type == Atom.BINDING_SITE) and (node_c.type == Atom.BINDING_SITE)):
411     71659   1258910784  17568.1      0.1                                  node_b_chemical.add_affinityDistance(node_b, node_c, self.chemistry.affinity(node_b.data, node_c.data))
412    587726   2706161481   4604.5      0.1                              node_b_changed = True
413                                                                   
414                                                               
415                                                       
416                                                       # If any of node b's rows have exceeded the cutoff distance, then remove them
417  47267073 239847142446   5074.3     10.5              for node_c, (distance_b_c, node_after_b) in node_b_distances.items(): # Can't use iteritems() here, as deleting from the dictionary
418  46716551 242694352980   5195.0     10.6                  if(distance_b_c > cutoff_distance):
419    200755    967443975   4819.0      0.0                      del node_b_distances[node_c]
420    200755    930470616   4634.9      0.0                      node_b_changed = True
421                                                               
422                                                               ## Affinity distances update
423    200755   4717125063  23496.9      0.2                      node_b_chemical.del_affinityDistance(node_b, node_c)
424                                                       
425                                                       # If we've modified node_b's distance table, tell its chemical to update accordingly
426    550522   2684634615   4876.5      0.1              if(node_b_changed):
427    235034   1383213780   5885.2      0.1                  node_b_chemical.nodes_changed.add(node_b)
428                                                   
429                                                   # Remove any neighbours that have infinite distance (have just unbound)
430                                                   ## TODO: not sure what difference it makes to do this here rather than above (after updating self.node_distances for neighbours)
431                                                   ##       but doing it above seems to break the walker's movement
432    791349   4367879451   5519.5      0.2          for (node_b, neighbour_distance_b_a) in self.neighbours[node_a].items(): # Can't use iteritems() here, as deleting from the dictionary
433    550522   2968919613   5392.9      0.1              if(neighbour_distance_b_a > cutoff_distance):
434       148       775638   5240.8      0.0                  del self.neighbours[node_a][node_b]
435                                                           
436                                                           ## Affinity distances update
437       148      2096343  14164.5      0.0                  self.del_affinityDistance(node_a, node_b)

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X