【问题标题】:Python performance comparison for creating sets - set() vs. {} literal [duplicate]用于创建集合的 Python 性能比较 - set() 与 {} 文字 [重复]
【发布时间】:2019-05-27 10:16:33
【问题描述】:

this question 之后的讨论让我感到疑惑,所以我决定运行一些测试并比较 set((x,y,z)){x,y,z} 在 Python 中创建集合的创建时间(我使用的是 Python 3.7)。

我使用timetimeit 比较了这两种方法。 两者都与以下结果一致*:

test1 = """
my_set1 = set((1, 2, 3))
"""
print(timeit(test1))

结果:0.30240735499999993

test2 = """
my_set2 = {1,2,3}
"""
print(timeit(test2))

结果:0.10771795900000003

所以第二种方法比第一种方法快了近 3 倍。 这对我来说是一个非常令人惊讶的差异。 以这种方式优化集合文字在 set() 方法上的性能的幕后发生了什么?对于哪些情况,哪种建议是可取的?

* 注意:我只显示timeit 测试的结果,因为它们是在许多样本上取平均值的,因此可能更可靠,但测试时的结果与time 在两种情况下表现出相似的差异。


编辑:我知道this similar question,虽然它回答了我最初问题的某些方面,但并没有涵盖所有问题。问题中没有解决集合,并且由于 empty sets 在 python 中没有文字语法,我很好奇使用文字创建集合(如果有的话)与使用 set() 有何不同方法。另外,我想知道set((x,y,z)tuple 参数 的处理是如何在幕后发生的,以及它对运行时可能产生的影响。 Coldspeed 的出色回答帮助解决了问题。

【问题讨论】:

  • 是的,空集文字不存在。非空的可以,您会发现对另一个问题的回答在很大程度上适用于您的问题。让我们希望没有人问关于元组文字与tuple(...) 的问题。
  • @AndrasDeak 这两个问题肯定是相关的,但我不太确定它们是否重复。当 set() 比文字构造/理解语法更合适时,这个问题没有解决,这似乎是这个 XY 问题中的底层 X。我不会自己关闭它,但如果它关闭,我不会失眠。
  • 这与[] vs list() 本质上是同一个问题。使文字语法更快的因素完全相同
  • 现代 Python 的欢乐时光:它有一个“空的 set 文字”,独眼猴子运算符:{*()}。它使用带有空 tuple 的解包泛化(这是 CPython 上的单例,因此实际上不会发生 tuple 构造)来强加必要的上下文,因此 Python 看到正在构造的 set,而不是 dict

标签: python python-3.x performance set


【解决方案1】:

(这是对现在已从初始问题中编辑的代码的回应)您忘记在第二种情况下调用函数。进行适当的修改,结果如预期:

test1 = """
def foo1():
     my_set1 = set((1, 2, 3))
foo1()
"""    
timeit(test1)
# 0.48808742000255734

test2 = """
def foo2():
    my_set2 = {1,2,3}
foo2()
"""    
timeit(test2)
# 0.3064506609807722

现在,时间差异的原因是因为 set() 是一个需要查找符号表的函数调用,而 {...} 集合构造是语法的产物,并且速度更快。

观察反汇编的字节码,区别很明显。

import dis

dis.dis("set((1, 2, 3))")
  1           0 LOAD_NAME                0 (set)
              2 LOAD_CONST               3 ((1, 2, 3))
              4 CALL_FUNCTION            1
              6 RETURN_VALUE

dis.dis("{1, 2, 3}")
  1           0 LOAD_CONST               0 (1)
              2 LOAD_CONST               1 (2)
              4 LOAD_CONST               2 (3)
              6 BUILD_SET                3
              8 RETURN_VALUE

在第一种情况下,函数调用是由指令 CALL_FUNCTION 对元组 (1, 2, 3) 进行的(它也有它自己的开销,虽然很小 - 它通过 LOAD_CONST 作为常量加载),而在第二条指令中只是一个BUILD_SET调用,效率更高。

Re:关于元组构建所用时间的问题,我们认为这实际上可以忽略不计:

timeit("""(1, 2, 3)""")
# 0.01858693000394851

timeit("""{1, 2, 3}""")
# 0.11971827200613916

元组是不可变的,因此编译器通过将其加载为常量来优化此操作——这称为constant folding(您可以从上面的LOAD_CONST 指令中清楚地看到这一点),因此所花费的时间可以忽略不计。这在集合中看不到,它们是可变的(感谢@user2357112 指出这一点)。


对于更大的序列,我们会看到类似的行为。 {..} 语法在使用集合解析构建集合时更快,而 set() 必须从生成器构建集合。

timeit("""set(i for i in range(10000))""", number=1000)
# 0.9775058150407858

timeit("""{i for i in range(10000)}""", number=1000)
# 0.5508635920123197

作为参考,您还可以在更新的版本上使用可迭代解包:

timeit("""{*range(10000)}""", number=1000)
# 0.7462548640323803

然而,有趣的是,set() 直接在 range 上调用时更快:

timeit("""set(range(10000))""", number=1000)
# 0.3746800610097125

这恰好比集合构造更快。您将看到其他序列的类似行为(例如 lists)。

我的建议是在构造集合文字时使用 {...} 集合推导,并作为将生成器推导传递给 set() 的替代方案;而是使用set() 将现有序列/可迭代转换为集合。

【讨论】:

  • 他也是在创建一个元组然后传给set函数,元组创建次数算不算?
  • @DanielMesejo 也许,但我不能确定。在这种情况下,可能不像我认为的那样 python 实习生(缓存)元组,所以在前几个 timeits 之后可能不会导致时间上有太大的差异。但从理论上讲,是的,它会有所贡献。
  • @DanielMesejo 可能是错误的,但从字节码来看,它似乎没有创建元组,它在最初解析 python 代码时被构建为常量。它只是 LOAD_CONSTs 元组。开销随之而来,从该元组构建集合。
  • Python 不实习 (1, 2, 3) 元组。你看到的是不断折叠,而不是实习。
  • @coldspeed: {1, 2, 3} 是可变的。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2023-04-01
  • 2011-02-07
  • 1970-01-01
  • 1970-01-01
  • 2014-04-24
  • 2012-02-05
  • 1970-01-01
相关资源
最近更新 更多