如何优化Python字典的使用?

60 投票
5 回答
3964 浏览
提问于 2025-04-16 11:14

问题:

我对我的Python程序进行了详细的性能分析,发现有一个函数让整个程序变得很慢。这个函数大量使用了Python字典,所以我可能没有用对它们。如果我无法让它运行得更快,我就得用C++重写它,所以有没有人能帮我在Python中优化一下?

我希望我提供的解释足够清楚,你们能理解我的代码!提前感谢任何帮助。

我的代码:

这是那个让人头疼的函数,我用line_profiler和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算法,会非常慢。(我最开始尝试过这个,结果比现在的版本慢了好几个数量级。)所以我的代码使用稀疏矩阵来表示完整距离矩阵的一个阈值版本(任何距离大于200单位的路径都会被忽略)。网络拓扑会随着时间变化,所以这个距离矩阵需要不断更新。为此,我使用了一种粗略的距离向量路由协议的实现:网络中的每个节点都知道到其他节点的距离和路径上的下一个节点。当拓扑发生变化时,相关的节点会相应地更新它们的距离表,并告诉它们的直接邻居。信息通过节点发送距离表给邻居的方式在网络中传播,邻居更新自己的距离表并再传播给它们的邻居。

有一个对象表示距离矩阵:self.node_distances。这是一个字典,将节点映射到路由表。节点是我定义的一个对象。路由表是一个字典,将节点映射到(距离,下一个节点)的元组。距离是从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会不断被外部修改,这来自于化学反应。这就是网络拓扑变化的来源。

我遇到问题的实际函数:propagate_distances_node()执行距离向量路由协议的一步。给定一个节点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)是距离矩阵的一个(小)子矩阵的缓存,用于程序的其他部分。

我这样做的原因是,我正在模拟参与反应的化学物质的计算类比,作为我博士研究的一部分。“化学物质”是一个“原子”(图中的节点)图。两个化学物质结合在一起被模拟为它们的两个图通过新边连接。化学反应发生(通过一个复杂的过程,这里不相关),改变图的拓扑。但反应中发生的事情取决于构成化学物质的不同原子之间的距离。因此,对于模拟中的每个原子,我想知道它与哪些其他原子是接近的。稀疏的、阈值的距离矩阵是存储这些信息的最有效方式。由于网络的拓扑在反应发生时会变化,我需要更新矩阵。距离向量路由协议是我能想到的最快的实现方式。我不需要更复杂的路由协议,因为在我特定的应用中,像路由循环这样的情况不会发生(因为我的化学物质的结构)。我之所以采用随机方式,是为了能够将化学反应过程与距离传播交错进行,模拟化学物质在反应发生时逐渐改变形状(而不是瞬间改变形状)。

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

更新:

我尝试将每个node_x == node_y的实例替换为node_x is node_y(根据@Sven Marnach的评论),但这反而让速度变慢了!(我没想到会这样!)我原来的性能分析运行了807.234秒,但这个修改后增加到895.895秒。抱歉,我之前的性能分析做错了!我使用了逐行分析,这在我的代码中变异太大(大约90秒的差异都是噪声)。在正确分析后,is确实比==快。使用CProfile时,我的代码用==运行了34.394秒,而用is时只用了33.535秒(我可以确认这是超出噪声的)。

更新:现有库

我不确定是否会有现成的库可以满足我的需求,因为我的要求比较特殊:我需要计算加权无向图中所有节点对之间的最短路径长度。我只关心低于阈值的路径长度。在计算路径长度后,我会对网络拓扑进行小的修改(添加或删除一条边),然后我想重新计算路径长度。我的图与阈值相比非常大(从给定节点开始,大部分图都在阈值之外),因此拓扑变化不会影响大多数最短路径长度。这就是我使用路由算法的原因:因为这可以将拓扑变化的信息传播到图结构中,这样我可以在信息传播超过阈值时停止。也就是说,我不需要每次都重新计算所有路径。我可以利用拓扑变化之前的路径信息来加快计算。这就是我认为我的算法会比任何库实现的最短路径算法快的原因。

我从未见过路由算法用于实际的物理网络之外的地方(但如果有人见过,我会很感兴趣)。

NetworkX是@Thomas K.推荐的。它有很多计算最短路径的算法。它有一个计算所有节点对最短路径长度的算法,并且可以设置截止值(这正是我想要的),但它只适用于无权图(而我的图是加权的)。不幸的是,它的加权图算法不允许使用截止值(这可能会让它们在我的图上变得很慢)。而且它的算法似乎都不支持在非常相似的网络上使用预计算的路径(也就是路由的东西)。

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

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

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

更新:内存使用情况

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

global memory usage my program's memory usage

我的程序没有耗尽内存而开始使用交换空间。从数字和IO图表没有活动可以看出这一点。IO图表上的尖峰是程序打印到屏幕上以显示其进度的地方。

然而,我的程序确实随着时间的推移不断使用更多的RAM,这可能不是好事(但总体上并没有使用太多的RAM,这就是我直到现在才注意到增加的原因)。

而且,IO图表上尖峰之间的距离随着时间的推移在增加。这是个坏消息——我的程序每100,000次迭代打印一次,所以这意味着每次迭代的执行时间随着时间的推移而变长……我通过长时间运行程序并测量打印语句之间的时间(程序每10,000次迭代之间的时间)确认了这一点。这应该是恒定的,但正如你从图中看到的,它线性增加……所以肯定有什么问题。(这个图的噪声是因为我的程序使用了很多随机数,所以每次迭代的时间会有所不同。)

lag between print statements increasing over time

在我的程序运行很长时间后,内存使用情况如下(所以肯定没有耗尽内存):

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

5 个回答

4

我觉得你的代码在性能方面没有什么问题(不考虑算法本身),你只是遇到了大量的循环次数。你代码中的某些部分执行了四千万次!

注意,80%的时间花在了20%的代码上,而这20%的代码就是那13行执行了超过2400万次。顺便提一下,你的代码很好地展示了帕累托原则(也就是“20%的喝酒者喝了80%的酒”)。

首先:你有没有试过Psycho? 这是一个即时编译器,可以大大加速你的代码——考虑到循环次数这么多,速度可能提升4到5倍——你只需要做的就是(当然,先下载和安装)在开头插入这段代码:

import psyco
psyco.full()

这就是我喜欢Psycho并在GCJ中使用它的原因,因为时间很重要——不需要额外编码,不会出错,只需添加两行就能获得显著提升。

回到细节问题(像把==换成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])当可以的时候。例如,我看到你在多个地方使用self.node_distances[node_a](第336、339、346、366、386行),这意味着每次使用时索引都要执行两次(一次是.,一次是[])——当执行数千万次时,这样会很耗时。看起来你可以在方法开始时这样做node_a_distances = self.node_distances[node_a],然后在后面的代码中使用这个变量。

6

你有没有考虑过 Pyrex 或者 Cython 呢?

它们可以把Python代码转成C语言,然后再自动生成.pyd文件,这样可能会让程序运行得更快,而且不需要太多额外的工作。

17

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 和 '==' 之间的差别不大。

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

你是不是在频繁交换数据?如果你的数据集太大,超出了可用的内存,我猜你可能会遇到与虚拟内存获取相关的输入输出争用问题。

你是在运行Linux吗?如果是的话,能否在运行你的程序时发一份你机器的vmstat?给我们发送类似下面的输出:

vmstat 10 100

祝你好运!

更新(来自提问者的评论)

我建议尝试调整 sys.setcheckinterval 并启用/禁用垃圾回收(GC)。这样做的原因是,对于这种特定情况(大量实例),默认的垃圾回收引用计数检查有点耗费资源,而它的默认间隔设置得太频繁了。

是的,我之前玩过 sys.setcheckinterval。我把它改成了 1000(从默认的 100),但没有明显的效果。禁用垃圾回收确实有帮助——谢谢。这是到目前为止最大的提速——节省了大约 20%(整个运行时间从171分钟降到135分钟)——我不确定这个结果的误差范围,但这肯定是一个统计上显著的提升。– Adam Nellis 2月9日 15:10

我的猜测:

我认为Python的垃圾回收是基于引用计数的。它会不时检查每个实例的引用计数;由于你在遍历这些庞大的内存结构,在你这种特定情况下,垃圾回收的默认频率(1000个周期?)实在是太频繁了——这是一种巨大的浪费。– Yours Truly 2月10日 2:06

撰写回答