- 问题:**
我已经把我的Python程序分析得要死了,有一个函数让所有的东西都慢了下来。它大量使用Python字典,所以我可能没有以最好的方式使用它们。如果我不能让它运行得更快,我将不得不用C++重写它,那么有人能帮我用Python优化它吗?
我希望我已经给出了正确的解释,希望你能理解我的代码!提前感谢你的帮助。
- 我的密码:**
这是使用line_profiler and kernprof分析的违规函数。
我尤其对第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
。这是一个将节点Map到路由表的字典。节点是我定义的一个对象。路由表是一个将节点Map到(distance,next_node). Distance是从node_a到node_b的图距离,并且next_node是你必须首先到达的node_a的邻居,在node_a和node_b之间的路径上。next_node为None表示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
通过化学React不断地从外部修改此函数,这就是网络拓扑变化的原因。
我遇到问题的实际函数是: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
)是距离矩阵的(小)子矩阵的缓存,由程序的不同部分使用。
我这么做的原因是我在模拟参与React的化学物质的计算模拟,这是我博士学位的一部分。"化学物质"是"原子"的图形。(图中的节点)。两种化学品结合在一起被模拟为它们的两个图被新边连接。发生化学React(通过与此无关的复杂过程),改变了图形的拓扑结构。但是React中发生了什么取决于组成化学物质的不同原子之间的距离。所以对于模拟中的每一个原子,我想知道它和其他哪些原子靠得很近。一个稀疏的、有阈值的距离矩阵是存储这些信息的最有效的方式。由于网络的拓扑结构随着React的发生而改变,我需要更新矩阵。distance-vector routing protocol是我能想到的最快的方法。我不需要更复杂的路由协议,因为在我的特定应用程序中不会发生路由环路之类的情况(因为我的化学物质是如何构造的)。我随机地做的原因是这样我就可以把化学React过程和传播的距离交织在一起,并模拟随着React发生而随时间逐渐改变形状的化学物质(而不是立即改变形状)。
此函数中的self
是表示化学品的对象。self.node_distances.keys()
中的节点是组成化学品的原子。self.node_distances[node_x].keys()
中的节点是来自化学品的节点,并且可能是来自与化学品结合(并与之React)的任何化学品的节点。
- 更新日期:**
我尝试将node_x == node_y
的每个示例替换为node_x is node_y
(根据@Sven Marnach的评论),但这让事情变慢了!(我没想到会这样!)我原来的配置文件需要807.234s才能运行,但经过修改后,它增加到了895.895s。抱歉,我的配置文件出错了!我使用的是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):
5条答案
按热度按时间yhived7q1#
node_after_b == node_a
将尝试调用node_after_b.__eq__(node_a)
:在使用C之前,尝试用优化版本覆盖
Node.__eq__()
。更新
我做了一个小实验(python 2.6.6):
结果:
我非常惊讶:
然后我尝试了更复杂的物体,结果与第一个实验一致。
如果您的数据集太大以至于无法容纳可用的RAM,我猜您可能会遇到与虚拟内存获取相关的某种I/O争用。
你运行的是Linux吗?如果是的话,你能在运行你的程序的时候发布一个你的机器的vmstat吗?给我们发送类似这样的输出:
祝你好运!
更新(来自OP的评论)
我建议使用sys.setcheckinterval并启用/禁用GC,理由是对于这种特殊情况(大量示例),默认的GC引用计数检查有点昂贵,而且它的默认间隔太频繁了。
是的,我以前玩过sys.setcheckinterval,我把它改成了1000(从默认的100),但它没有做任何可测量的差异。禁用垃圾收集有所帮助-谢谢。这是迄今为止最大的加速-节省约20%(全程171分钟,下降到135分钟)-我不知道误差线是多少,但它肯定是一个统计上显著的增长。-Adam Nellis 2月9日15:10
我猜:
我认为Python GC是基于引用计数的,它会不时地检查每个示例的引用计数;由于您要遍历这些巨大的内存结构,因此在您的特定情况下,GC默认频率(1000个周期?)太频繁了--这是一种巨大的浪费。
ua4mk5z42#
您是否考虑过Pyrex/Cython?
它会自动将python编译成C,然后再编译成.pyd,所以它可能会在不做太多工作的情况下加快速度。
cunj1qz13#
这将需要相当数量的工作,但是...你可以考虑使用在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.YMMV上花费了大约0.002s,但是对于任何显著并行的东西,GPU很可能是一个长期的赢家,而且由于它是一个经过充分研究的算法,你应该能够利用现有的高度调优的代码。
对于Python / GPU桥,我建议使用PyCUDA或PyOpenCL。
ki0zmccv4#
我看不出你的代码在性能方面有什么问题(没有尝试去研究算法),你只是被大量的迭代所打击。你的代码的一部分被执行了4000万次!
请注意80%的时间是如何花在20%的代码上的--而这13行代码被执行了2400多万次。顺便说一句,通过这段代码,您为Pareto principle(或“20%的啤酒饮用者饮用了80%的啤酒”)提供了很好的说明。
要事优先:你试过Psycho吗?这是一个JIT编译器,可以大大提高代码的速度--考虑到大量的迭代--比如说4 - 5倍--你所要做的(当然是在下载和安装之后)就是在开头插入这个代码片段:
这就是为什么我喜欢Psycho,并在GCJ中也使用了它,在那里时间是至关重要的-没有代码,没有出错,并从添加的2行中突然增强。
回到吹毛求疵的问题上(因为时间上的%改进很小,所以会发生变化,比如用
is
替换==
等等)。A)除了你提到的代码行之外,我注意到#388在执行
node_b_update = False
时花费了相当多的时间。哦,等等--每次执行它时,都会在全局范围内查找False
!为了避免这种情况,在该方法的开头赋值F, T = False, True
,并将后面使用的False
和True
替换为局部变量F
和T
。这应该减少总时间,尽管只有一点点(3%?)。B)我注意到#389中的情况“仅”出现了512,120次(基于#390的执行次数)与#391中的条件(16,251,407)。由于没有相关性,颠倒这些检查的顺序是有意义的--因为早期的“削减”应该不会给予什么推动作用我不确定完全避免
pass
语句是否会有帮助,但如果不损害可读性:C)我刚刚注意到你在一个情况下使用
try-except
(#365-367)你只需要字典中的默认值-尝试使用.get(key, defaultVal)
或者用collections.defaultdict(itertools.repeat(float('+inf')))
创建你的字典。使用try-除了有它的代价-见#365报告3.5%的时间,这是设置堆栈帧和诸如此类的东西。D)避免索引访问(可能的话,可以使用obj
.
字段或obj[
idx]
)。例如,我看到您在多个地方使用self.node_distances[node_a]
(编号336、339、346、366、386),这意味着对于每次使用索引被使用两次(一次用于.
,一次用于[]
)--当执行数千万次时,代价会很高。在我看来,你可以只在node_a_distances = self.node_distances[node_a]
开始的方法上做,然后进一步使用它。w1jd8yoj5#
我本想把这个作为我问题的更新发布,但是堆栈溢出只允许在问题中包含30000个字符,所以我把这个作为答案发布。
**更新:**我迄今为止的最佳优化
我采纳了人们的建议,现在我的代码运行速度比以前快了21%,这很好--谢谢大家!
这是迄今为止我做得最好的一次,我将所有
==
测试替换为针对节点的is
测试,禁用垃圾收集,并按照@ NasBanov的建议重写了第388行的大if
语句部分,还添加了众所周知的try/except
技巧来避免测试(第390行-删除测试node_c in node_b_distances
),这有助于加载,因为它几乎不抛出异常。我试着切换第391行和第392行,并将node_b_distances[node_c]
赋给一个变量,但这种方法是最快的。但是,我仍然没有找到内存泄漏(见我的问题中的图表)。但是我认为这可能是在我的代码的不同部分(我还没有在这里发布)。如果我能修复内存泄漏,那么这个程序将运行得足够快,我可以用途:)