【问题标题】:list.count() vs Counter() performancelist.count() 与 Counter() 性能
【发布时间】:2019-12-02 01:15:39
【问题描述】:

在尝试查找字符串中一堆字符的频率时,为什么对 4 个不同的字符运行 string.count(character) 4 次会比使用集合产生更快的执行时间(使用 time.time())。计数器(字符串)?

背景: 给定由字符串表示的一系列移动。有效的移动是 R(右)、L(左)、U(上)和 D(下)。如果移动序列将我带回原点,则返回 True。否则,返回假。


# approach - 1 : iterate 4 times (3.9*10^-6 seconds)
def foo1(moves):
    return moves.count('U') == moves.count('D') and moves.count('L') == moves.count('R')

# approach - 2 iterate once (3.9*10^-5 seconds)
def foo2(moves): 
    from collections import Counter
    d = Counter(moves)
    return d['R'] == d['L'] and d['U'] == d['D']

import time
start = time.time()
moves = "LDRRLRUULRLRLRLRLRLRLRLRLRLRL"
foo1(moves)
# foo2(moves)
end = time.time()
print("--- %s seconds ---" % (end - start))

这些结果与我的预期相反。我的理由是第一种方法应该花费更长的时间,因为字符串迭代了 4 次以上,而在第二种方法中,我们只迭代了一次。可能是由于库调用开销造成的吗?

【问题讨论】:

  • 请提供一个可重现的例子。请注意,您的第一个“应用程序”除了定义一个函数之外什么都不做……您的实际字符串是什么?第二种方法可能会缩放更好,但实际上对于小字符串可能不会更快。
  • @juanpa.arrivillaga 我添加了更多细节。我想你可能是对的。但是,这个问题来自 leetcode leetcode.com/problems/robot-return-to-origin,我得到的结果是(方法 1 为 20 ms,方法 2 为 90 ms,这让我感到疑惑)。他们通常有输入很长的边缘测试用例,但我无法确定,因为我看不到他们在此上运行的测试用例。
  • 旁注:一个小的优化机会,不管你怎么做,如果输入字符串的长度是奇数,则返回False。由于返回原点必须将每一步都与相反的移动配对,所以奇数长度肯定不会通过。

标签: python-3.x count counter performance-testing performancecounter


【解决方案1】:

Counter理论上更快,但固定开销更高,尤其是与str.count相比,它可以通过直接内存比较来扫描底层C数组,其中list.count必须这样做每个元素的丰富比较;将moves 转换为单个字符的list 几乎是本地测试中foo1 时间的三倍,从448 ns 到1.3 μs(而foo2 实际上快了一点,从5.6 μs 下降到5.48 μs) .

其他问题:

  1. 导入已导入的模块会使用缓存导入,但即使是缓存导入也会产生惊人的开销(加载机制需要检查很多东西以确保一切正常这样做);在本地测试中,将from collections import Counter 移动到顶层将foo2 的运行时间减少了 1.6 μs(单个全局导入为 5.6 μs,本地每次调用导入为 7.2 μs)。这会因环境而异很多;在另一台机器上(在用户和系统站点包中安装的东西较少),开销仅为 0.75 μs。无论如何,这对 foo2 来说是一个可以避免的重大劣势。
  2. 现代 Python 上的Counter 使用 C 加速器来加速计数,但只有当迭代足够长时,加速器才会提供好处。如果您使用 moveslist 形式,但将其乘以 100 以获得更长的序列,则相对而言,差异会下降(foo1 为 106 µs,foo2 为 140 µs)
  3. 你只是没有计算很多东西;当您只关心四件事时,支付O(n) 四次很容易超过支付一次O(n) 如果前一种情况具有较低的常数乘数(不包含在大O 符号中) 比后者。 Counter 仍然是 O(n) 用于计算任意数量的独特事物;每次调用 .countO(n),但是如果您需要知道输入中每个唯一事物的计数,对于大多数唯一的输入,每个单独的 .count 调用将渐近 O(n²)。李>
  4. .count 方法在您的特定情况下是短路的,所以 它甚至没有做 O(n) 工作四次,只是两次UD 计数不匹配,因此它根本不计算 LRCounter 如果不能短路(所有成本都在单次计数中支付),它不会变得有意义地变慢,但是你的 foo1,在我从第 2 点开始使用的同一基准测试中(更长的输入,在list 形式中),如果我只是将一个D 添加到(预乘)moves 的末尾(使UD 计数相同,则从 106 µs 到 185 µs,并且需要另外两个count 调用); foo2 只上升到 143 µs(从 140 µs),大概是因为 moves 实际上变长了(在乘以 100 之前添加 D 意味着它从 2900 个元素增加到 3000 个)。

基本上,你有一些小的实现弱点,但大多数情况下,你碰巧选择了一个给.count带来所有优势的用例,Counter没有。如果你的输入总是str,而你只是counting 他们很少,固定的次数,那么可以肯定,重复调用count 通常会赢。但是对于任意输入类型(尤其是迭代器,其中count 是不可能的,因为它不存在,并且因为您只能迭代一次),尤其是较大的输入类型,需要计算更多独特的东西,其中一致的性能很重要(所以依靠短路来减少count 调用的数量是不可接受的),Counter 会赢。

【讨论】:

  • 精湛的分析!太感谢了! :)
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2016-01-08
  • 1970-01-01
  • 2014-04-22
  • 2013-10-13
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多