如何优化 Python 中大型(75,000 项)布尔值集的操作?
Posted
技术标签:
【中文标题】如何优化 Python 中大型(75,000 项)布尔值集的操作?【英文标题】:How to optimize operations on large (75,000 items) sets of booleans in Python? 【发布时间】:2011-04-27 10:09:54 【问题描述】:有一个名为svnmerge.py 的脚本,我正在尝试对其进行调整和优化。不过,我对 Python 完全陌生,所以这并不容易。
当前的问题似乎与脚本中名为RevisionSet
的类有关。本质上,它所做的是创建一个包含整数键布尔值的大型哈希表(?)。在最坏的情况下 - 我们的 SVN 存储库中的每个修订版本都有一个,现在接近 75,000 个。
之后,它对如此庞大的数组执行集合操作——加法、减法、交集等等。该实现是最简单的 O(n) 实现,自然会在如此大的集合上变得相当慢。可以优化整个数据结构,因为有很长的连续值跨度。例如,从 1 到 74,000 的所有键都可能包含 true
。此外,该脚本是为 Python 2.2 编写的,这是一个相当旧的版本,而且我们仍然使用 2.6,所以在那里也可能会有所收获。
我可以尝试自己拼凑它,但这会很困难并且需要很多时间 - 更不用说它可能已经在某个地方实现了。虽然我喜欢学习经验,但现在结果更重要。你会建议我做什么?
【问题讨论】:
你想对布尔列表执行什么操作?一个 numpy 布尔值数组会对你有帮助吗? 这个 set 实现看起来是 O(n),而不是 O(n*m)。if r in rs
其中rs
是一个 dict 是一个 O(1) 操作,而不是 O(len(rs))。
@Baffe Boyois - 是的,想想看。修正了问题文本。
至少值得用内置插件替换旧的 Python 支持(rsplit 等),这些插件可能是 C 实现的。
【参考方案1】:
你可以尝试用 numpy 而不是普通的 python 来做。对于此类操作,我发现它非常快。
例如:
# Create 1000000 numbers between 0 and 1000, takes 21ms
x = numpy.random.randint(0, 1000, 1000000)
# Get all items that are larger than 500, takes 2.58ms
y = x > 500
# Add 10 to those items, takes 26.1ms
x[y] += 10
由于行数更多,我认为 75000 也不应该成为问题 :)
【讨论】:
好的,我去看看。如果我最终使用它,我会接受你的回答。 就个人而言,我不认为这里真的需要 numpy。我认为 Python 的内置集合已经足够了。 如果你想减少内存占用,你也可以告诉 numpy 使用 8 位整数。但是,我不确定您是否可以使用 randint 函数执行此操作。 docs.scipy.org/doc/numpy/user/basics.types.html @GWW:不能直接使用 randint,但您甚至可以告诉 numpy 将其转换为bool
,如下所示:numpy.array(numpy.random.randint(0, 2, 1000000), dtype='bool')
@Justin Peel:足够了。但我猜大多数操作 numpy 会更快。它专为此类大规模操作而设计。【参考方案2】:
这里有一个快速替换 RevisionSet 的方法,可以将它变成一个集合。它应该快得多。我没有完全测试它,但它适用于我所做的所有测试。毫无疑问,还有其他方法可以加快速度,但我认为这真的很有帮助,因为它实际上利用了集合的快速实现,而不是在 Python 中执行循环,而原始代码在 __sub__
和 __and__
等函数中执行循环。唯一的问题是迭代器没有排序。您可能需要更改一些代码来解决此问题。我相信还有其他方法可以改进这一点,但希望它会给你一个好的开始。
class RevisionSet(set):
"""
A set of revisions, held in dictionary form for easy manipulation. If we
were to rewrite this script for Python 2.3+, we would subclass this from
set (or UserSet). As this class does not include branch
information, it's assumed that one instance will be used per
branch.
"""
def __init__(self, parm):
"""Constructs a RevisionSet from a string in property form, or from
a dictionary whose keys are the revisions. Raises ValueError if the
input string is invalid."""
revision_range_split_re = re.compile('[-:]')
if isinstance(parm, set):
print "1"
self.update(parm.copy())
elif isinstance(parm, list):
self.update(set(parm))
else:
parm = parm.strip()
if parm:
for R in parm.split(","):
rev_or_revs = re.split(revision_range_split_re, R)
if len(rev_or_revs) == 1:
self.add(int(rev_or_revs[0]))
elif len(rev_or_revs) == 2:
self.update(set(range(int(rev_or_revs[0]),
int(rev_or_revs[1])+1)))
else:
raise ValueError, 'Ill formatted revision range: ' + R
def sorted(self):
return sorted(self)
def normalized(self):
"""Returns a normalized version of the revision set, which is an
ordered list of couples (start,end), with the minimum number of
intervals."""
revnums = sorted(self)
revnums.reverse()
ret = []
while revnums:
s = e = revnums.pop()
while revnums and revnums[-1] in (e, e+1):
e = revnums.pop()
ret.append((s, e))
return ret
def __str__(self):
"""Convert the revision set to a string, using its normalized form."""
L = []
for s,e in self.normalized():
if s == e:
L.append(str(s))
else:
L.append(str(s) + "-" + str(e))
return ",".join(L)
补充: 顺便说一句,我比较了原始 RevisionSet 和上面我的 RevisionSet 的并集、交集和减法,当对两个具有 75000 个元素的 RevisionSet 进行操作时,上面的代码对于这些操作的速度要快 3 倍到 7 倍。我知道其他人说 numpy 是要走的路,但是如果您对 Python 不是很有经验,正如您的评论所表明的那样,那么您可能不想走那条路,因为它将涉及更多的更改。我建议尝试我的代码,看看它是否有效,如果有效,然后看看它对你来说是否足够快。如果不是,那么我会尝试分析以查看需要改进的地方。只有这样我才会考虑使用 numpy(这是一个我经常使用的很棒的包)。
【讨论】:
@Vilx,如果您只是将文件中调用 sorted 方法的 3 个点替换为 just sorted(theRevSet),则可以删除它 那不是递归栈溢出?我今天开始阅读 python 教程,但还没有上课。 :P 不是,因为RevisionSet类的排序方法与函数不同。换句话说, sorted 函数不调用 sorted 方法。如果您查看一个集合,它没有任何称为排序的方法。如果你愿意,你可以保留旧的排序方法,但我不会。【参考方案3】:例如,从 1 到 74,000 的所有键都包含 true
为什么不对子集工作?就74001到最后。
修剪 74/75 的数据比尝试编写比 O(n) 更聪明的算法要容易得多。
【讨论】:
@Vilx:怎么会这样?您只需要对事物进行子集化。 我想你可能误解了我的意思。这些不是真实的数字,只是我当场编造的。我的意思是,相同的布尔值有很大的区间。 @Vilx:我想你可能误解了我的意思。如果您有无关紧要的范围,请找到一种方法将它们过滤掉。此外,请尝试在您的问题中坚持事实——误导性的数值会给您误导性的答案。 好的,用“可能”这个词更新了问题。无论如何,这正是我想要做的——改变这个类(只有这个类,我不想改变它在代码中的使用方式),以便优化范围。 @Vlix:删除未使用的值不是“优化”吗?添加过滤器有什么问题?【参考方案4】:您应该重写 RevisionSet 以获得一组修订。我认为修订的内部表示应该是一个整数,并且应该根据需要创建修订范围。
没有令人信服的理由使用支持 python 2.3 及更早版本的代码。
【讨论】:
【参考方案5】:只是一个想法。我曾经在二进制图像处理中使用运行编码来做这种事情。也就是说,将每个集合存储为一系列数字:关闭的位数、打开的位数、关闭的位数等。
然后您可以对它们进行各种布尔运算,作为简单合并算法的装饰。
【讨论】:
以上是关于如何优化 Python 中大型(75,000 项)布尔值集的操作?的主要内容,如果未能解决你的问题,请参考以下文章
具有大型(70,000+ 项)数据集的高效 jQuery 实时搜索