【发布时间】: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_affinityDistance 和 del_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 == b、a is b和a is a。对我来说,is始终比==快约 25%。我只是无法想象自定义类的实例为什么会反过来。这两个运算符的唯一区别是==首先在instances字典中查找__eq__(),然后回退到is执行的相同操作,并且查找需要一些时间。 -
networkx.dijkstra_path() 算法中实际上有一个用于加权无向网络的截止参数。除了路径长度之外,它还会计算 paths ,因此它可能具有比您需要的更多的内存开销。修改该代码以不存储路径将相当简单。此外,networkx.dijkstra_predecessor and_and_distance() 可能对您来说很有趣,因为它会跟踪最短路径中的邻居(前身)。
标签: python optimization dictionary sparse-matrix