关于创建集合的python性能比较:创建集合的python性能比较 – set() vs. {} literal

Python performance comparison for creating sets - set() vs. {} literal

本问题已经有最佳答案,请猛点这里访问。

在这个问题之后的讨论让我感到疑惑,所以我决定运行一些测试,比较一下在Python中创建集合(我使用的是Python3.7)的set((x,y,z)){x,y,z}的创建时间。

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

1
2
3
4
test1 ="""
my_set1 = set((1, 2, 3))
"""

print(timeit(test1))

结果:0.3024073549999993

1
2
3
4
test2 ="""
my_set2 = {1,2,3}
"""

print(timeit(test2))

结果:0.10771795900000003

所以第二种方法比第一种方法快3倍。这对我来说是一个相当惊人的不同。在这种情况下,在set()方法的基础上,如何优化set-literal的性能?哪种情况比较合适?

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

编辑:我知道这个类似的问题,虽然它回答了我最初问题的某些方面,但它并没有涵盖所有问题。在这个问题中,集合没有被解决,而且由于空集合在python中没有文本语法,我很好奇(如果有的话)使用文本创建的集合与使用set()方法有什么不同。另外,我想知道在后台如何处理set((x,y,z)中的tuple参数,以及它对运行时的可能影响。Coldspeed的伟大回答有助于解决问题。


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

1
2
3
4
5
6
7
test1 ="""
def foo1():
     my_set1 = set((1, 2, 3))
foo1()
"""
   
timeit(test1)
# 0.48808742000255734
1
2
3
4
5
6
7
test2 ="""
def foo2():
    my_set2 = {1,2,3}
foo2()
"""
   
timeit(test2)
# 0.3064506609807722

现在,计时不同的原因是因为set()是一个需要查找符号表的函数调用,而{...}集构造是语法的产物,速度快得多。

观察反汇编的字节码时,差异是明显的。

1
2
3
4
5
6
7
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
1
2
3
4
5
6
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

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

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

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

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

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

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

1
2
3
4
5
timeit("""set(i for i in range(10000))""", number=1000)
# 0.9775058150407858

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

作为参考,您还可以在较新版本上使用ITerable解包:

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

有趣的是,当直接调用range时,set()更快:

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

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

我的建议是在构造集合字面值时使用{...}集合理解,并作为将生成器理解传递给set()的替代方法;相反,使用set()将现有序列/iterable转换为集合。