【问题标题】:Why is initializing a set in python as `{*()}` faster than `set()`?为什么在 python 中将集合初始化为 `{*()}` 比 `set()` 快?
【发布时间】:2021-03-30 03:43:42
【问题描述】:

我有兴趣找到在 Python 中初始化空集的最快方法(我使用的是 3.8)。您不能将空集实例化为{},因为这会创建dict,因此通常建议使用set() 构造函数。前几天我注意到还有另一种实例化空集的方法:您可以将空元组 () 解压缩到集的 {...} 语法中,如下所示:{*()}。使用ipython 中的timeit 模块对此进行计时会得到以下结果:

%timeit {*()}
67.7 ns ± 1.68 ns per loop (mean ± std. dev. of 7 runs, 10000000 loops each)

%timeit set()
84.5 ns ± 2.57 ns per loop (mean ± std. dev. of 7 runs, 10000000 loops each)

我觉得这很奇怪 - 更优雅的 set() 构造函数相对于 {*()} 花费的时间多 25%。过去也有相同的观察结果,例如,[] vs. list(){} vs. dict()

%timeit []
17.8 ns ± 0.791 ns per loop (mean ± std. dev. of 7 runs, 100000000 loops each)
%timeit list()
81 ns ± 1.56 ns per loop (mean ± std. dev. of 7 runs, 10000000 loops each)

%timeit {}
18.6 ns ± 0.575 ns per loop (mean ± std. dev. of 7 runs, 100000000 loops each)
%timeit dict()
98.6 ns ± 5.09 ns per loop (mean ± std. dev. of 7 runs, 10000000 loops each)

对于lists 和dicts,切换到原始语法{}[] 相当容易,但是我发现的集合显然不是那么干净。我很好奇对此的见解(一般而言)。

【问题讨论】:

  • 这种性能差异是如此之小,以至于出于所有意图和目的,只需使用您认为最易读的内容。但这很可能是函数调用开销的结果,这对 Python 来说是相当大的。
  • 总的来说我完全同意。在某些算法中,我可以看到非平凡的加速——但这更多是因为我对内部机制的好奇,而不是其他任何事情。

标签: python python-3.x set benchmarking python-internals


【解决方案1】:
>>> import dis
>>> dis.dis("{*()}")
  1           0 LOAD_CONST               0 (())
              2 BUILD_SET_UNPACK         1
              4 RETURN_VALUE
>>> dis.dis("set()")
  1           0 LOAD_NAME                0 (set)
              2 CALL_FUNCTION            0
              4 RETURN_VALUE

这里有两件事更快。

首先,set 需要全局查找,LOAD_NAME 通常是两个哈希查找(一个在全局命名空间中,一个在内置命名空间中)。然后是函数调用,这在 Python 中是昂贵的。

将此与LOAD_CONST(一种快速、单一的数组索引操作)和BUILD_SET_UNPACK(一种专门的集合构建指令)进行比较。毫无疑问,它最终会完成 set 构造函数所做的事情,但基本上在这里你最终会使用字节码级别的快捷方式)。

无论如何,出于所有目的,请坚持使用更易读的set()

【讨论】:

    【解决方案2】:

    我发现盯着接近的细节往往会导致误导性结论。

    如果你真的对结果做一些事情,set() 会更快。

    例如:

    from timeit import timeit
    
    
    def do_literal():
        s = {*()}
        s.add(1)
    
    
    def do_function():
        s = set()
        s.add(1)
    
    
    print(timeit(do_literal, number=100000000))
    print(timeit(do_function, number=100000000))
    

    结果:

    14.3875497
    13.313828099999998
    

    为什么会出现差异是另一个有趣的问题,但也许不是 SO(我当然不知道答案)。

    【讨论】:

    • 非常好。虽然,我怀疑,如果你想做很多小事,性能将大致相同。
    • 我怀疑这与结果集的初始容量有关,集合构造函数被调整为允许有效地进行多次添加的容量,而文字版本被调整为允许最小的过剩容量.也许尝试比较set()set(())。我很想看看结果。
    • 一个类似的例子,[None]*n 将创建一个列表,其中没有过度分配初始缓冲区,所以第一个 .append 会很慢,因为它需要重新分配那个缓冲区。但是,[None for _ in range(n)] 可能会导致缓冲区过度分配,因此.append 会非常快。
    • 这是一个非常有趣的测试!我正在理解@juanpa.arrivillaga 所说的 - 在许多情况下,存在空容器的目的是为了,例如有效的比较、占位符等,并且可能永远不会填充项目 - 因此不需要预先分配
    【解决方案3】:

    set() 是一个需要查找符号表的函数调用,而集合构造文字是“语法的产物”。对字节码的对等方尤其明显。

    See here 以获得更深入的答案

    【讨论】:

      【解决方案4】:

      我推测,类似于dict(){}list[] 的情况,它具有涉及函数调用的不同结构。查看dis的输出:

      import dis
      >>> dis.dis('set()')
        1           0 LOAD_NAME                0 (set)
                    2 CALL_FUNCTION            0
                    4 RETURN_VALUE
      >>> dis.dis('{*()}')
        1           0 LOAD_CONST               0 (())
                    2 BUILD_SET_UNPACK         1
                    4 RETURN_VALUE
      
      

      Python 中的函数调用很昂贵,因为它们 have high extra overhead

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 2016-05-28
        • 2020-02-22
        • 1970-01-01
        • 1970-01-01
        • 2014-06-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多