2011-02-04 10 views
57

Pregunta:Optimización del pitón código de acceso diccionario

que he perfilado mi programa de Python para la muerte, y no hay una función que se está desacelerando todo abajo. Utiliza diccionarios de Python en gran medida, por lo que puede que no los haya usado de la mejor manera. Si no puedo ejecutarlo más rápido, tendré que volver a escribirlo en C++, entonces ¿hay alguien que pueda ayudarme a optimizarlo en Python?

¡Espero haber dado el tipo correcto de explicación, y que usted puede dar sentido a mi código! Gracias de antemano por cualquier ayuda.

Mi código:

Ésta es la función problemática, perfilada mediante line_profiler and kernprof. Estoy ejecutando Python 2.7

Estoy particularmente desconcertado por cosas como las líneas 363, 389 y 405, donde una declaración if con una comparación de dos variables parece tomar una cantidad de tiempo desorbitada.

He considerado usar NumPy (como lo hace con las matrices dispersas) pero no creo que sea apropiado porque: (1) No estoy indexando mi matriz usando enteros (estoy usando instancias de objeto); y (2) No estoy almacenando tipos de datos simples en la matriz (estoy almacenando tuplas de un flotante y una instancia de objeto). Pero estoy dispuesto a ser persuadido sobre NumPy. Si alguien sabe acerca del escaso rendimiento de la matriz de NumPy frente a las tablas hash de Python, me interesaría.

Lo siento, no he dado un ejemplo simple que se puede ejecutar, pero esta función está vinculada a un proyecto mucho más grande y no pude encontrar la manera de configurar un ejemplo simple para probarlo, sin darle la mitad de mi código base!

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(): 
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) 

Explicación de mi código:

Esta función mantiene una matriz de distancia escasa que representa la distancia de red (suma de los pesos de borde en la ruta más corta) entre los nodos en un (muy grande) de red. Para trabajar con la tabla completa y usar el Floyd-Warshall algorithm sería muy lento. (Intenté esto primero, y fue en órdenes de magnitud más lentos que la versión actual.) Entonces mi código usa una matriz dispersa para representar una versión con umbral de la matriz de distancia completa (se ignoran las rutas con una distancia superior a 200 unidades). La topología de la red cambia con el tiempo, por lo que esta matriz de distancia necesita actualizarse a lo largo del tiempo. Para hacer esto, estoy usando una implementación aproximada de distance-vector routing protocol: cada nodo en la red conoce la distancia entre cada nodo y el siguiente nodo en la ruta. Cuando ocurre un cambio de topología, los nodos asociados con este cambio actualizan sus tablas de distancia en consecuencia y se lo dicen a sus vecinos inmediatos. La información se propaga a través de la red por nodos que envían sus tablas de distancia a sus vecinos, quienes actualizan sus tablas de distancia y las difunden a sus vecinos.

Hay un objeto que representa la matriz de distancia: self.node_distances. Este es un mapa de nodos de mapeo a tablas de enrutamiento. Un nodo es un objeto que he definido. Una tabla de enrutamiento es un diccionario de asignación de nodos a tuplas de (distancia, siguiente_nodo). La distancia es la distancia gráfica entre node_a y node_b, y next_node es el vecino de node_a al que debe ir primero, en la ruta entre node_a y node_b. Un next_node de None indica que node_a y node_b son vecinos de gráfico.Por ejemplo, una muestra de una matriz de distancia podría ser:

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

Debido a cambios en la topología, dos nodos que eran muy separados (o no conectado en todo) puede llegar a ser de cerca. Cuando esto sucede, las entradas se agregan a esta matriz. Debido al umbral, dos nodos pueden separarse demasiado para preocuparse. Cuando esto sucede, las entradas se eliminan de esta matriz.

La matriz self.neighbours es similar a self.node_distances, pero contiene información sobre los enlaces directos (bordes) en la red. self.neighbours continuamente se modifica externamente a esta función, por la reacción química. Aquí es de donde provienen los cambios de topología de red.

La función real con la que estoy teniendo problemas: propagate_distances_node() realiza un paso del distance-vector routing protocol. Dado un nodo, node_a, la función se asegura de que los vecinos node_a estén correctamente en la matriz de distancia (cambios de topología). La función luego envía la tabla de enrutamiento node_a a todos los vecinos inmediatos de node_a en la red. Integra la tabla de enrutamiento node_a con la tabla de enrutamiento de cada vecino.

En el resto de mi programa, se llama repetidamente a la función propagate_distances_node(), hasta que la matriz de distancia converja. Se mantiene un conjunto, self.nodes_changed, de los nodos que han cambiado su tabla de rutas desde la última actualización. En cada iteración de mi algoritmo, se elige un subconjunto aleatorio de estos nodos y se llama a ellos al propagate_distances_node(). Esto significa que los nodos distribuyen sus tablas de enrutamiento de forma asíncrona y estocástica. Este algoritmo converge en la matriz de distancia real cuando el conjunto self.nodes_changed se vacía.

Las partes de "distancias de afinidad" (add_affinityDistance y del_affinityDistance) son un caché de una (pequeña) submatriz de la matriz de distancia, que es utilizada por una parte diferente del programa.

La razón por la que hago esto es porque estoy simulando análogos computacionales de sustancias químicas que participan en reacciones, como parte de mi doctorado. Un "producto químico" es un gráfico de "átomos" (nodos en el gráfico). Dos sustancias químicas que se unen juntas se simulan cuando sus dos gráficos se unen por nuevos bordes. Se produce una reacción química (mediante un proceso complicado que no es relevante aquí), cambiando la topología del gráfico. Pero lo que sucede en la reacción depende de qué tan separados estén los diferentes átomos que componen los productos químicos. Entonces, para cada átomo en la simulación, quiero saber a qué otros átomos se aproxima. Una matriz de distancia escasa y con umbral es la forma más eficiente de almacenar esta información. Como la topología de la red cambia a medida que se produce la reacción, necesito actualizar la matriz. Un distance-vector routing protocol es la forma más rápida que se me ocurre de hacer esto. No necesito un protocolo de enrutamiento más compatible, porque cosas como los bucles de enrutamiento no ocurren en mi aplicación particular (debido a la estructura de mis productos químicos). La razón por la que lo hago estocásticamente es para poder intercalar los procesos de reacción química con la dispersión de la distancia, y simular un químico que cambia gradualmente de forma a medida que ocurre la reacción (en lugar de cambiar de forma instantáneamente).

El self en esta función es un objeto que representa un producto químico. Los nodos en self.node_distances.keys() son los átomos que componen el producto químico. Los nodos en self.node_distances[node_x].keys() son nodos del químico y potencialmente nodos de cualquier químico al que el producto químico está unido (y reacciona).

Actualización:

intenté reemplazar todas las instancias de node_x == node_y con node_x is node_y (según el comentario de @Sven Marnach), pero se desaceleró cosas! (¡No esperaba eso!) Mi perfil original tomó 807.234s para ejecutarse, pero con esta modificación aumentó a 895.895. Disculpa, estaba haciendo el perfil equivocado Estaba usando line_by_line, que (en mi código) tenía demasiada variación (esa diferencia de ~ 90 segundos estaba en el ruido). Al perfilarlo correctamente, is es infinitamente más rápido que ==. Usando CProfile, mi código con == tomó 34.394s, pero con is, tomó 33.535s (que puedo confirmar que está fuera del ruido).

Actualización: Las bibliotecas existentes

estoy seguro de si habrá una biblioteca existente que puede hacer lo que quiera, ya que mis necesidades son inusuales: necesito para calcular la ruta más corta longitudes entre todos los pares de nodos en un gráfico ponderado, no dirigido. Solo me importan las longitudes de ruta que sean inferiores a un valor umbral. Después de calcular las longitudes de las rutas, realizo un pequeño cambio en la topología de la red (agregando o eliminando un borde), y luego quiero volver a calcular las longitudes de la ruta. Mis gráficos son enormes en comparación con el valor umbral (desde un nodo dado, la mayoría del gráfico está más alejado que el umbral), por lo que los cambios de topología no afectan a la mayoría de las longitudes de ruta más cortas. Esta es la razón por la que estoy usando el algoritmo de enrutamiento: porque esto propaga la información de cambio de topología a través de la estructura del gráfico, por lo que puedo dejar de propagarla cuando se haya ido más allá del umbral. es decir, no necesito volver a calcular todas las rutas cada vez. Puedo usar la información de ruta anterior (desde antes del cambio de topología) para acelerar el cálculo. Esta es la razón por la que creo que mi algoritmo será más rápido que cualquier implementación de la biblioteca de algoritmos de ruta más corta. Nunca he visto algoritmos de enrutamiento utilizados fuera de enrutar paquetes a través de redes físicas (pero si alguien tiene, entonces estaría interesado).

NetworkX fue sugerido por @Thomas K. Tiene lots of algorithms para calcular las rutas más cortas. Tiene un algoritmo para calcular el all-pairs shortest path lengths con un punto de corte (que es lo que quiero), pero solo funciona en gráficos sin ponderar (los míos son ponderados). Desafortunadamente, su algorithms for weighted graphs no permite el uso de un límite (lo que puede hacer que se vuelvan lentos para mis gráficos). Y ninguno de sus algoritmos parece ser compatible con el uso de rutas precalculadas en una red muy similar (es decir, el material de enrutamiento).

igraph es otra biblioteca de gráfico que conozco, pero al mirar its documentation, no encuentro nada sobre las rutas más cortas. Pero me podría haber perdido, su documentación no parece muy completa.

NumPy puede ser posible, gracias al comentario de @ 9000. Puedo almacenar mi matriz dispersa en una matriz NumPy si asigno un entero único a cada instancia de mis nodos. Luego puedo indexar una matriz NumPy con enteros en lugar de instancias de nodo. También necesitaré dos matrices NumPy: una para las distancias y otra para las referencias de "next_node". Esto podría ser más rápido que usar diccionarios de Python (aún no lo sé).

¿Alguien sabe de alguna otra biblioteca que pueda ser útil?

Actualización: Uso de memoria

Estoy utilizando Windows (XP), asi que aquí hay algo de información sobre el uso de memoria, desde Process Explorer. El uso de la CPU es del 50% porque tengo una máquina de doble núcleo.

global memory usage my program's memory usage

Mi programa no se ejecuta sin memoria RAM y comienza a golpear el intercambio. Puede ver eso a partir de los números, y del gráfico IO que no tiene ninguna actividad. Los picos en el gráfico IO son donde el programa imprime en la pantalla para decir cómo está.

Sin embargo, mi programa no siga usando más y más memoria RAM con el tiempo, lo que probablemente no es una buena cosa (pero no está consumiendo la cantidad de RAM en general, por lo que no me di cuenta el aumento hasta el momento).

Y la distancia entre los picos en el gráfico IO aumenta con el tiempo. Esto es malo: mi programa imprime en la pantalla cada 100.000 iteraciones, lo que significa que cada iteración tarda más en ejecutarse a medida que pasa el tiempo ... Lo he confirmado haciendo un largo recorrido de mi programa y midiendo el tiempo transcurrido entre declaraciones de impresión (el tiempo entre cada 10.000 iteraciones del programa). Esto debería ser constante, pero como se puede ver en el gráfico, aumenta linealmente ... entonces algo está ahí arriba. (El ruido en este gráfico es porque mi programa utiliza una gran cantidad de números aleatorios, por lo que el tiempo para cada iteración varía.)

lag between print statements increasing over time

Después de mi programa ha estado funcionando durante mucho tiempo, el uso de la memoria es el siguiente (por lo que definitivamente no quedarse sin RAM):

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

+10

Ojalá todas las preguntas tuvieran este tipo de contenido, me entristece no poder ayudarte. – Leigh

+0

¿Has considerado la posibilidad de paralelizar tu algoritmo? Si no puede hacer los cálculos centrales más rápido, aún podrá acelerarlo si tiene múltiples núcleos disponibles. – samtregar

+7

En cuanto a las comparaciones, le desconcierta: el operador '==' realmente llama al método '__eq __()' del objeto comparado. Si todo lo que quiere saber es identidad de objeto, use 'is' en su lugar, esto será considerablemente más rápido. –

Respuesta

17

node_after_b == node_a tratará de llamar 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 
>>> 

intenta reemplazar Node.__eq__() con una versión optimizada antes de recurrir a C.

ACTUALIZACIÓN

I hizo que este pequeño experimento (pitón 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() 

Resultados:

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 

Me sorprendió mucho:

  • "dot" access (ob.property) parece ser muy caro (línea 25 frente a la línea 27).
  • no hubo mucha diferencia entre que es y '==', al menos para los objetos simples

Luego probé con los objetos más complejos y los resultados son consistentes con el primer experimento.

¿Está intercambiando mucho? Si su conjunto de datos es tan grande que no cabe en la memoria RAM disponible, supongo que puede experimentar algún tipo de conflicto de E/S relacionado con las recuperaciones de memoria virtual.

¿Está ejecutando Linux? Si es así, ¿podría publicar un vmstat de su máquina mientras ejecuta su programa? Envíenos la salida de algo así como:

vmstat 10 100 

Buena suerte!

ACTUALIZACIÓN (a partir de los comentarios de acuerdo OP)

I sugested jugar con sys.setcheckinterval y activar/desactivar el GC. El fundamento es que, para este caso particular (gran cantidad de instancias), la verificación de recuento de referencias GC por defecto es algo costosa y su intervalo predeterminado es demasiado frecuente.

Sí, anteriormente había jugado con sys.setcheckinterval. Lo cambié a 1000 (de su valor predeterminado de 100), pero no hizo ninguna diferencia medible. Desactivar la recolección de basura ha ayudado a , gracias. Este ha sido el mayor aumento de velocidad hasta ahora - ahorrando cerca de 20% (171 minutos para toda la carrera, abajo a 135 minutos) - No estoy seguro de lo que las barras de error son en eso, pero debe ser una estadísticamente significativo aumento. - Adam Nellis Feb 9 de la 15:10

Mi suposición:

creo que el GC Python se basa en cuenta de referencia. De vez en cuando, comprobará el recuento de referencias para en cada instancia; como está atravesando estas enormes estructuras en memoria , en su caso particular la frecuencia por defecto del GC (1000 ciclos?) está ausente con demasiada frecuencia - un gran desperdicio . - Atentamente, 10 de febrero a las 2:06

+0

Gracias por la información, pero no he definido una función '__eq __()' para mis nodos. Entonces, supongo que Python usa una función predeterminada que compara los ID de los objetos (que deberían ser rápidos)? Para mis nodos, "iguales" e "instancias idénticas" son la misma cosa. Además, traté de reemplazar mis pruebas '==' con 'is', ¡pero eso empeoró las cosas! (Consulte la actualización de mi pregunta.) –

+0

Gracias por la actualización, muy completa. Estaba equivocado sobre 'es' empeorar las cosas - He cambiado '==' a 'is' en mi código y obtuve una pequeña aceleración. No estoy llenando mi memoria RAM, pero mi programa está usando más memoria RAM con el tiempo y disminuyendo la velocidad con el tiempo. Supongo que los dos están conectados. Estoy ejecutando Windows, así que hice lo que pude para darte un equivalente de vmstat. Ver mi pregunta actualizada para más detalles. –

+0

Bueno, parece que no es E/S de disco. ¡Me estoy quedando sin teorías! Por cierto, ¿has jugueteado con sys.setcheckinterval? –

4

Esto requerirá una buena cantidad de trabajo, pero ... podría considerar usar Floyd-Warshall con una GPU. Se ha trabajado mucho para lograr que Floyd-Warshall funcione de manera muy eficiente en una GPU. Una rápida de los rendimientos de Google:

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

A pesar de que, tal como se aplica en Python, Floyd-Warshall fue más lento en un orden de magnitud, una una buena versión de GPU en una poderosa GPU aún podría superar significativamente a su nuevo código de Python.

Aquí hay una anécdota. Tuve una pieza de código breve, simple y de uso intensivo de cómputo que hizo algo similar a una acumulación de datos. En Python, optimizado como pude obtenerlo, tomó ~ 7s en un veloz i7. Luego escribí una versión de GPU completamente no optimizada; tomó ~ 0.002s en una Nvidia GTX 480. YMMV, pero para cualquier cosa significativamente paralela, la GPU es probable que sea un ganador a largo plazo, y dado que es un algoritmo bien estudiado, debería ser capaz de utilizar el código altamente afinado existente .

Para el puente de Python/GPU, recomendaría PyCUDA o PyOpenCL.

6

¿Has considerado Pyrex/Cython?

Compila python a C y luego a .pyd automáticamente, por lo que puede acelerar un poco las cosas sin mucho trabajo.

4

No veo nada de malo en su código con respecto al rendimiento (sin tratar de asimilar el algoritmo), simplemente está siendo golpeado por la gran cantidad de iteraciones. Partes de su código se ejecutan 40 millones veces!

Observe cómo se gasta el 80% del tiempo en el 20% de su código, y esas son las 13 líneas que se ejecutan más de 24 millones de veces. Por cierto, con este código se proporciona una gran ilustración al Pareto principle (o "el 20% de los bebedores de cerveza beben el 80% de la cerveza").

Lo primero es lo primero: ha intentado Psycho? Es un compilador JIT que puede acelerar mucho tu código, teniendo en cuenta la gran cantidad de iteraciones, por ejemplo, de 4x-5x, y todo lo que tienes que hacer (después de descargar e instalar, por supuesto) es insertar este fragmento en el Principio:

import psyco 
psyco.full() 

es por esto que me gustó Psycho y lo utilizó en GCJ también, donde el tiempo es esencial - nada al código, nada para conseguir impulso repentino mal y de 2 líneas añadido.

Volver a nit-picking (que cambia como reemplazar == con is etc. es, debido a la pequeña mejora del% de tiempo). Aquí están las 13 líneas "por su culpa":

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) además de las líneas que mencionas, noto que # 388 tiene relativamente alto momento en que es trivial, lo único que hace es node_b_update = False. Ah, pero espera: ¡cada vez que se ejecuta, False se busca en el alcance global! Para evitar eso, asigne F, T = False, True en el comienzo del método y reemplace los usos posteriores de False y True con los locales F y T. Esto debería disminuir el tiempo total, aunque por poco (3%?).

B) Noto que la condición en # 389 ocurrió "solo" 512,120 veces (basado en el número de ejecuciones de # 390) frente a la condición en # 391 con 16,251,407. Como no hay dependencia, tiene sentido invertir el orden de esos controles, debido al "corte" temprano que debería dar poco impulso (¿2%?). No estoy seguro de si pass evitando por completo las declaraciones ayudará, pero si no se pierde nada legibilidad:

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) me he dado cuenta de que está utilizando try-except en un caso (# 365-367) sólo tiene valor por defecto de un diccionario - intente usar en su lugar .get(key, defaultVal) o cree sus diccionarios con collections.defaultdict(itertools.repeat(float('+inf'))). Usar try-except tiene su precio - ver # 365 informes 3.5% del tiempo, eso es configurar marcos de pila y otras cosas.

D) Evite el acceso indexado (ya sea con obj . campo u obj [ idx ]) cuando sea posible.Por ejemplo, veo que usa self.node_distances[node_a] en varios lugares (# 336, 339, 346, 366, 386), lo que significa que para cada uso, la indexación se usa dos veces (una para . y una para []), y eso se vuelve costoso cuando se ejecutan decenas de millones de veces. Me parece que puede hacer en el método que comienza en node_a_distances = self.node_distances[node_a] y luego usar eso más.

+0

Sí, lo he intentado con Psyco. No ayudó mi código, pero gracias por mencionarlo. Estoy ejecutando 2.7 porque tiene una función hash mucho mejor para almacenar instancias de objetos en diccionarios (ver [mi pregunta anterior] (http://stackoverflow.com/questions/4865325/counting-collisions-in-a-python-dictionary) para detalles). (A) Esto fue realmente interesante - es curioso cómo algunas cosas que son súper eficientes en algunos idiomas (asignando un literal) se vuelven lentas en otros. (B) Esto es bueno. He tomado más en cuenta sus sugerencias (vea mi respuesta, la habría publicado como una actualización, ¡pero SO no me lo permitió!). –

+0

@ Adam Nellis: ah, pero 'True' y' False' no son literales, ¡resulta! :) Tuve que hacer algunas pruebas para convencerme, como 'False, True = True, False' y' def f(): return False' y luego 'print f(); Falso = 7; imprimir f(); del False; print f() ' –

+0

@Adam: me sorprende que no hayas experimentado ningún% de aumento de velocidad significativo al usar' psyco'. tal vez psyco y y @profile no se mezclen. ver mis artículos agregados (c) y (d) arriba. –

1

Lo habría publicado como una actualización de mi pregunta, pero Stack Overflow solo permite 30000 caracteres en las preguntas, así que estoy publicando esto como una respuesta.

Actualización: Mis mejores optimizaciones hasta ahora

he tomado en la junta sugerencias de las personas, y ahora mi código se ejecuta alrededor del 21% más rápido que antes, lo cual es bueno - gracias a todos!

Esto es lo mejor que he podido hacer hasta ahora. He reemplazado todas las pruebas == con is para nodos, deshabilité la recolección de basura y volví a escribir la gran parte de la declaración if en la Línea 388, en línea con las sugerencias de @Nas Banov. Añadí el conocido truco try/except para evitar las pruebas (línea 390 - para eliminar la prueba node_c in node_b_distances), que ayudó a cargar, ya que casi nunca arroja la excepción. Traté de cambiar las líneas 391 y 392, y la asignación de node_b_distances[node_c] a una variable, pero de esta manera fue la más rápida.

Sin embargo, todavía no he rastreado la pérdida de memoria (vea el gráfico en mi pregunta). Pero creo que esto podría estar en una parte diferente de mi código (que no he publicado aquí). Si puedo arreglar la fuga de memoria, entonces este programa se ejecutará lo suficientemente rápido como para que lo use :)

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)): 
8 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) 
+0

¿Has probado Python 3.2? Probé iterando a través de un diccionario y con 3.2 "para ... en x.items()" era ~ 30% más rápido que "para ... en x.iteritems()" y mucho más rápido que "para ... x. artículos()". – casevh

Cuestiones relacionadas