【问题标题】:Optimising Python dictionary access code优化 Python 字典访问代码
【发布时间】:2011-06-21 12:10:18
【问题描述】:

问题:

我已经彻底分析了我的 Python 程序,并且有一个函数让一切都变慢了。它大量使用 Python 字典,所以我可能没有以最好的方式使用它们。如果我不能让它运行得更快,我将不得不用 C++ 重新编写它,那么有没有人可以帮助我在 Python 中优化它?

我希望我给出了正确的解释,并且希望您能够理解我的代码!提前感谢您的帮助。

我的代码:

这是有问题的函数,使用line_profiler and kernprof 进行了分析。我正在运行 Python 2.7

我对第 363、389 和 405 行等内容感到特别困惑,其中比较两个变量的 if 语句似乎花费了过多的时间。

我考虑过使用NumPy(因为它处理稀疏矩阵),但我认为它不合适,因为:(1)我没有使用整数索引我的矩阵(我正在使用对象实例); (2) 我没有在矩阵中存储简单的数据类型(我正在存储浮点数和对象实例的元组)。 但我愿意接受 NumPy 的说服。 如果有人知道 NumPy 的稀疏矩阵性能与 Python 的哈希表,我会很感兴趣。

对不起,我没有给出一个可以运行的简单示例,但是这个函数被绑定在一个更大的项目中,我不知道如何设置一个简单的示例来测试它,没有给你一半我的代码库!

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)

我的代码解释:

此函数维护一个稀疏距离矩阵,表示(非常大的)网络中节点之间的网络距离(最短路径上的边权重之和)。使用完整的表并使用Floyd-Warshall algorithm 会非常慢。 (我首先尝试了这个,它比当前版本慢了几个数量级。)所以我的代码使用稀疏矩阵来表示全距离矩阵的阈值版本(任何距离大于 200 个单位的路径都被忽略)。网络拓扑随时间变化,因此该距离矩阵需要随时间更新。为此,我使用了distance-vector routing protocol 的粗略实现:网络中的每个节点都知道到其他节点和路径上下一个节点的距离。当拓扑发生变化时,与此变化相关的节点会相应地更新它们的距离表,并告诉它们的直接邻居。信息通过网络传播,节点将距离表发送给邻居,邻居更新距离表并将其传播给邻居。

有一个表示距离矩阵的对象:self.node_distances。这是一个将节点映射到路由表的字典。节点是我定义的对象。路由表是将节点映射到 (distance, next_node) 的元组的字典。 distance 是从 node_a 到 node_b 的图距离,next_node 是 node_a 的邻居,你必须首先到达,在 node_a 和 node_b 之间的路径上。 None 的 next_node 表示 node_a 和 node_b 是图邻居。例如,距离矩阵的样本可以是:

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

由于拓扑结构的变化,两个相距很远(或根本没有连接)的节点可能会变得很近。发生这种情况时,会将条目添加到此矩阵中。由于阈值,两个节点可能会变得太远而无法关心。发生这种情况时,将从该矩阵中删除条目。

self.neighbours 矩阵类似于self.node_distances,但包含有关网络中直接链接(边)的信息。 self.neighbours 通过化学反应不断地被外部修改为这个函数。这就是网络拓扑变化的来源。

我遇到问题的实际功能:propagate_distances_node() 执行distance-vector routing protocol 的一步。给定一个节点node_a,该函数确保node_a 的邻居在距离矩阵中正确(拓扑变化)。然后该函数将node_a 的路由表发送到网络中node_a 的所有直接邻居。它将node_a 的路由表与每个邻居自己的路由表集成在一起。

在我的程序的其余部分,propagate_distances_node() 函数被重复调用,直到距离矩阵收敛。维护了一组self.nodes_changed,其中包含自上次更新以来已更改其路由表的节点。在我的算法的每次迭代中,都会选择这些节点的随机子集,并在它们上调用 propagate_distances_node()。这意味着节点以异步和随机的方式传播它们的路由表。当集合self.nodes_changed 变为空时,该算法收敛于真实距离矩阵。

“亲和距离”部分(add_affinityDistancedel_affinityDistance)是距离矩阵的(小)子矩阵的缓存,由程序的不同部分使用。

我这样做的原因是我正在模拟参与反应的化学物质的计算类似物,这是我攻读博士学位的一部分。 “化学”是“原子”(图中的节点)的图。结合在一起的两种化学物质被模拟为它们的两个图被新边连接起来。发生化学反应(通过一个在这里不相关的复杂过程),改变了图的拓扑结构。但反应中发生的情况取决于构成化学物质的不同原子之间的距离。所以对于模拟中的每个原子,我想知道它靠近哪些其他原子。稀疏的阈值距离矩阵是存储此信息的最有效方式。由于网络的拓扑会随着反应的发生而变化,因此我需要更新矩阵。 distance-vector routing protocol 是我能想到的最快的方法。我不需要更复杂的路由协议,因为在我的特定应用程序中不会发生路由循环之类的事情(因为我的化学品的结构)。我随机地这样做的原因是,我可以将化学反应过程与距离扩展交错,并模拟化学反应随着时间的推移逐渐改变形状(而不是立即改变形状)。

此函数中的self 是一个表示化学物质的对象。 self.node_distances.keys() 中的节点是构成化学物质的原子。 self.node_distances[node_x].keys() 中的节点是来自化学品的节点,并且可能来自与化学品结合(并与之反应)的任何化学品的节点。

更新:

我尝试用node_x is node_y 替换node_x == node_y 的每个实例(根据@Sven Marnach 的评论),但它减慢了速度! (我没想到!) 我原来的配置文件需要 807.234 秒才能运行,但经过这次修改后,它增加到了 895.895 秒。 抱歉,我做错了配置文件!我使用的是 line_by_line,它(在我的代码上)有太多的差异(大约 90 秒的差异都在噪音中)。正确分析它时,is 肯定比== 快。使用CProfile,我的代码使用== 花费了34.394 秒,但使用is,花费了33.535 秒(我可以确认这是没有噪音的)。

更新: 现有库

我不确定是否会有一个现有的库可以做我想做的事,因为我的要求不寻常: 我需要计算加权无向图中所有节点对之间的最短路径长度。我只关心低于阈值的路径长度。计算路径长度后,我对网络拓扑进行了小幅更改(添加或删除边),然后我想重新计算路径长度。与阈值相比,我的图很大(从给定节点来看,大部分图都比阈值更远),因此拓扑变化不会影响大多数最短路径长度。这就是我使用路由算法的原因:因为它通过图结构传播拓扑变化信息,所以当它超过阈值时我可以停止传播它。即,我不需要每次都重新计算所有路径。我可以使用之前的路径信息(从拓扑更改之前)来加快计算速度。这就是为什么我认为我的算法将比最短路径算法的任何库实现更快。 我从未见过在通过物理网络实际路由数据包之外使用的路由算法(但如果有人有,那么我会感兴趣)。

NetworkX 由@Thomas K 建议。 它有lots of algorithms 用于计算最短路径。 它有一个算法用于计算带有截止值的all-pairs shortest path lengths(这是我想要的),但它只适用于未加权的图(我的是加权的)。 不幸的是,它的algorithms for weighted graphs 不允许使用截止(这可能会使它们在我的图表中变慢)。而且它的算法似乎都不支持在非常相似的网络上使用预先计算的路径(即路由的东西)。

igraph 是我知道的另一个图形库,但是查看its documentation,我找不到关于最短路径的任何信息。但我可能错过了——它的文档似乎不是很全面。

NumPy 可能是可能的,感谢@9000 的评论。如果我为节点的每个实例分配一个唯一的整数,我可以将我的稀疏矩阵存储在 NumPy 数组中。然后我可以用整数而不是节点实例来索引 NumPy 数组。我还需要两个 NumPy 数组:一个用于距离,一个用于“next_node”引用。这可能比使用 Python 字典更快(我还不知道)。

有人知道其他可能有用的库吗?

更新:内存使用情况

我运行的是 Windows (XP),所以这里有一些关于内存使用的信息,来自Process Explorer。 CPU 使用率是 50%,因为我有一台双核机器。

我的程序没有用完 RAM 并开始命中交换。您可以从数字中看到这一点,并且从 IO 图中没有任何活动。 IO 图上的尖峰是程序打印到屏幕上的位置,以说明它的运行情况。

但是,随着时间的推移,我的程序确实会占用越来越多的 RAM,这可能不是一件好事(但它总体上并没有占用太多 RAM,这就是为什么我直到现在才注意到增加的原因)。

IO 图上的尖峰之间的距离随着时间的推移而增加。这很糟糕 - 我的程序每 100,000 次迭代就会打印到屏幕上,这意味着随着时间的推移,每次迭代都需要更长的时间来执行......我已经通过长时间运行我的程序并测量之间的时间来确认这一点打印语句(程序每 10,000 次迭代之间的时间)。这应该是恒定的,但正如您从图表中看到的那样,它线性增加......所以有些东西在那里。 (这张图的噪音是因为我的程序使用了很多随机数,所以每次迭代的时间会有所不同。)

我的程序运行了很长时间后,内存使用情况是这样的(所以它肯定没有用完RAM):

【问题讨论】:

  • 我希望所有的问题都有这样的内容,让我很难过我真的不能帮助你。
  • 关于比较你感到困惑的是:操作符==实际上调用了比较对象的方法__eq__()。如果您只想知道对象身份,请改用is——这样会快很多。
  • 您是否经常重新创建存储在矩阵中的对象实例?如果不这样做,您可以为它们分配一个整数范围,将它们全部放入具有相应索引的列表中,您将能够将识别索引存储在 NumPy 数组中并将其用作索引。
  • @Adam:我刚刚在不同版本的 Python 中做了一些测试。我定义了几个类(没有一个实现__eq__())和定时的东西,比如a == ba is ba is a。对我来说,is 始终比== 快约 25%。我只是无法想象自定义类的实例为什么会反过来。这两个运算符的唯一区别是==首先在instances字典中查找__eq__(),然后回退到is执行的相同操作,并且查找需要一些时间。
  • networkx.dijkstra_path() 算法中实际上有一个用于加权无向网络的截止参数。除了路径长度之外,它还会计算 paths ,因此它可能具有比您需要的更多的内存开销。修改该代码以不存储路径将相当简单。此外,networkx.dijkstra_predecessor and_and_distance() 可能对您来说很有趣,因为它会跟踪最短路径中的邻居(前身)。

标签: python optimization dictionary sparse-matrix


【解决方案1】:

node_after_b == node_a 将尝试调用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
>>> 

在使用 C 之前尝试用优化版本覆盖 Node.__eq__()

更新

我做了这个小实验(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()

结果:

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

我很惊讶:

  • “点”访问(ob.property)似乎非常昂贵(第 25 行与第 27 行)。
  • is 和 '==' 之间没有太大区别,至少对于简单对象而言

然后我尝试了更复杂的对象,结果与第一个实验一致。

你经常交换吗?如果您的数据集太大以至于无法容纳可用的 RAM,我想您可能会遇到某种与虚拟内存提取相关的 I/O 争用。

你在运行 Linux 吗?如果是这样,您可以在运行程序时发布您机器的 vmstat 吗?向我们发送类似以下内容的输出:

vmstat 10 100

祝你好运!

更新(来自 OP 的 cmets)

我建议使用 sys.setcheckinterval 并启用/禁用 GC。理由是对于这种特殊情况(大量实例),默认的 GC 引用计数检查有点昂贵,而且它的默认间隔太频繁了。

是的,我以前玩过 sys.setcheckinterval。我把它改成 1000(默认为 100),但它 没有做任何可衡量的差异。 禁用垃圾收集有 帮助 - 谢谢。这一直是 迄今为止最大的加速 - 节省大约 20%(整个跑步 171 分钟, 减少到 135 分钟) - 我不确定 错误栏是什么,但是 它必须具有统计显着性 增加。 – Adam Nellis 2 月 9 日 15:10

我的猜测:

我认为 Python GC 是基于 参考计数。时不时的 将检查参考计数 每个实例;既然你是 遍历这些巨大的内存 结构,在您的特定情况下 GC 默认频率(1000 周期?)太频繁了 - 一个巨大的 浪费。 – Yours Truly 2 月 10 日 2:06

【讨论】:

  • 感谢您的信息,但我还没有为我的节点定义 __eq__() 函数。所以我认为 Python 使用了一个默认函数来比较对象 ID(应该很快)?对于我的节点,“相等”和“相同实例”是一回事。另外,我尝试用is 替换我的== 测试,但这让事情变得更糟! (请参阅我的问题的更新。)
  • 感谢您的更新 - 非常全面。我错了 is 让事情变得更糟 - 我在我的代码中将 == 更改为 is 并获得了微小的加速。我没有填满我的 RAM,但随着时间的推移,我的程序正在使用更多的 RAM - 并且随着时间的推移变慢 - 我认为这两者是连接的。我正在运行 Windows,所以我已尽我所能为您提供相当于 vmstat 的功能。有关详细信息,请参阅我更新的问题。
  • 嗯,好像不是磁盘I/O。我的理论已经用完了!顺便说一句,你有没有摆弄 sys.setcheckinterval?
  • 另一个想法:既然你有足够的内存,请尝试禁用 GC 一段时间(gc.disable/gc.enable)。
  • @Paulo Scardine 现在很流行……但你有我的 +1
【解决方案2】:

这需要大量的工作,但是……您可以考虑使用在 GPU 上运行的 Floyd-Warshall。为了让 Floyd-Warshall 在 GPU 上非常高效地运行,已经做了很多工作。快速谷歌搜索产生:

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

尽管在 Python 中实现 Floyd-Warshall 的速度要慢一个数量级,但功能强大的 GPU 上的良好 GPU 版本仍可能显着优于您的新 Python 代码。

这是一个轶事。我有一段简短、简单、计算密集型的代码,它做了类似于霍夫累加的事情。在 Python 中,我可以得到优化,在快速 i7 上花费了大约 7 秒。然后我写了一个完全非优化的GPU版本;在 Nvidia GTX 480 上花费了大约 0.002 秒。YMMV,但对于任何显着并行的东西,GPU 很可能是长期的赢家,而且由于它是一个经过充分研究的算法,你应该能够利用现有的高度调整的代码.

对于 Python / GPU 桥,我推荐 PyCUDA 或 PyOpenCL。

【讨论】:

    【解决方案3】:

    你考虑过Pyrex/Cython吗?

    它会自动将 python 编译为 C,然后自动编译为 .pyd,因此它可以在不做太多工作的情况下加快速度。

    【讨论】:

      【解决方案4】:

      我认为您的代码在性能方面没有任何问题(没有尝试了解算法),您只是受到大量迭代的打击。您的部分代码被执行了 40 百万次!

      请注意 80% 的时间是如何花在 20% 的代码上的——这 13 行代码执行了 24+ 百万次。顺便说一句,您使用此代码为Pareto principle 提供了很好的说明(或“20% 的啤酒饮用者喝了 80% 的啤酒”)。

      首要任务:你试过Psycho吗?它是一个 JIT 编译器,可以大大加快你的代码 - 考虑到大量的迭代 - 比如说 4x-5x 倍 - 你所要做的(当然是在下载和安装之后)就是插入这个 sn-p一开始:

      import psyco
      psyco.full()
      

      这就是为什么我喜欢 Psycho 并在 GCJ 中也使用它的原因,时间很重要 - 无需编写代码,无需出错,添加的 2 行代码突然提升。

      回到 nit-picking (改变如用 is 替换 == 等,因为时间改进很小)。这是“错误”的 13 行:

      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) 除了你提到的那几行,我注意到#388 有相对较高的时间,当它是微不足道的时候,它就做到了node_b_update = False。哦,但等等 - 每次执行时,False 都会在全局范围内查找!为避免这种情况,请在方法的开头指定F, T = False, True,并用本地FT 替换以后使用的FalseTrue。这应该会减少总时间,尽管会减少一点点(3%?)。

      B) 我注意到 #389 中的条件“仅”发生了 512,120 次(基于 #390 的执行次数),而 #391 中的条件发生了 16,251,407 次。由于没有依赖关系,因此颠倒这些检查的顺序是有意义的——因为早期的“削减”应该不会带来什么提升(2%?)。我不确定完全避免 pass 语句是否会有所帮助,但如果它不损害可读性:

      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) 我刚刚注意到您在一个案例 (#365-367) 中使用 try-except,您只需要字典中的默认值 - 尝试使用 .get(key, defaultVal) 代替,或者使用 collections.defaultdict(itertools.repeat(float('+inf'))) 创建您的字典。使用 try-except 是有代价的 - 请参阅 #365 报告 3.5% 的时间,即设置堆栈帧等等。

      D) 尽可能避免索引访问(使用 obj.field 或 obj[idx])。例如,我看到您在多个地方(#336、339、346、366、386)使用self.node_distances[node_a],这意味着每次使用索引都会使用两次(一次用于.,一次用于[])-并且执行数千万次时变得昂贵。在我看来,您可以使用以node_a_distances = self.node_distances[node_a] 开头的方法,然后进一步使用。

      【讨论】:

      • 是的,我试过 Psyco。它对我的代码没有帮助,但感谢您提及它。我正在运行 2.7,因为它具有更好的散列函数,可以将对象实例存储在字典中(有关详细信息,请参阅my previous question)。 (A) 这真的很有趣——有趣的是,在某些语言中一些超级高效的东西(分配文字)在其他语言中变得很慢。 (B) 这是好东西。我已经进一步采纳了您的建议(请参阅我的回答 - 会将其作为更新发布,但 SO 不会让我这样做!)。
      • @Adam Nellis:啊,但事实证明,TrueFalse 不是文字! :) 我必须做一些测试来说服自己,比如False, True = True, Falsedef f(): return False 然后print f(); False = 7; print f(); del False; print f()
      • @Adam:我很惊讶你没有体验到使用psyco 带来的任何显着的速度提升。也许 psyco 和 @profile 不能混用。请参阅上面我添加的 (c) 和 (d) 项。
      【解决方案5】:

      我会将此作为对我的问题的更新发布,但 Stack Overflow 仅允许问题中包含 30000 个字符,因此我将其发布为答案。

      更新:迄今为止我的最佳优化

      我采纳了人们的建议,现在我的代码运行速度比以前快了大约 21%,这很好 - 谢谢大家!

      这是迄今为止我能做到的最好的。根据@Nas Banov 的建议,我已将所有== 测试替换为is 用于节点,禁用垃圾收集并在第388 行重写了if 大语句部分。我添加了著名的 try/except 避免测试技巧(第 390 行 - 删除测试 node_c in node_b_distances),这有助于加载,因为它几乎不会抛出异常。我尝试切换第 391 行和第 392 行,并将 node_b_distances[node_c] 分配给一个变量,但这种方式是最快的。

      但是,我还没有找到内存泄漏(请参阅我的问题中的图表)。但我认为这可能在我的代码的不同部分(我没有在这里发布)。如果我能修复内存泄漏,那么这个程序将运行得足够快,我可以使用:)

      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)
      

      【讨论】:

      • 你试过 Python 3.2 吗?我测试了遍历字典和 3.2“for ... in x.items()”比“for ... in x.iteritems()”快约 30%,比“for ... x.项目()“。
      猜你喜欢
      • 2016-07-02
      • 2011-04-15
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2017-03-13
      • 2017-09-03
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多