dict.__setitem__(key, x) 是否比 dict[key] = x 慢(或者快),为什么?

4

我发现了一些奇怪的东西。

我定义了两个测试函数,如下所示:

def with_brackets(n=10000):
    d = dict()
    for i in range(n):
        d["hello"] = i

def with_setitem(n=10000):
    d = dict()
    st = d.__setitem__
    for i in range(n):
        st("hello", i)

我们希望这两个功能的执行速度大致相同。然而:

>>> timeit(with_brackets, number=1000)
0.6558860000222921

>>> timeit(with_setitem, number=1000)
0.9857697170227766

可能有些地方我没有注意到,但看起来setitem的时间几乎是dict[key] = x的两倍长,我不是很理解为什么。难道dict[key] = x不应该调用__setitem__吗?

(使用CPython 3.9)

编辑:改用timeit而不是time


1
不要使用 time.time 进行代码的计时,它是不可靠的。使用 timeit - Mark Ransom
1
在使用 n=10000000 进行这些计算大约 25 次后,我有两个观察结果:1)我相信有时间差异,2)每次写入 d.__setitem__("hello", n) 的速度甚至比您当前设置 setitemd.__setitem__ 的方法更慢。 - Kraigolas
@Kraigolas,我知道d.__setitem__速度比较慢,所以我把它从循环中拿掉了,认为这是导致速度慢的原因,但即使没有它,速度仍然很慢,这让我非常困惑。 - Phobia
1
我用你的代码尝试了 timeit,得到了类似的结果 - with_brackets 大约比 with_setitem 快两倍。而且我也认为括号会在内部转换为对 __setitem__ 的调用,所以我无法解释这个结果。 - Mark Ransom
1
对于CPython来说,d["hello"] 调用 PyObject_SetItem,该函数直接调用 dict_ass_sub(通过指针 mp_ass_subscript),而 st = d.__setitem__; st("hello", n) 则将该 C 调用封装在 Python 调用中(如 metatoaster 所示),这会引入更多的开销。 - YiFei
1个回答

6

dict[key] = x应该会调用__setitem__方法吧?

严格来说,不是的。将两个函数都通过dis.dis进行分析,我们得到以下结果(此处只包括for循环):

>>> dis.dis(with_brackets)
...
        >>   22 FOR_ITER                12 (to 36)
             24 STORE_FAST               3 (i)

  5          26 LOAD_FAST                0 (n)
             28 LOAD_FAST                1 (d)
             30 LOAD_CONST               1 ('hello')
             32 STORE_SUBSCR
             34 JUMP_ABSOLUTE           22
...

Vs

>>> dis.dis(with_setitem)
...
        >>   28 FOR_ITER                14 (to 44)
             30 STORE_FAST               4 (i)

  6          32 LOAD_FAST                2 (setitem)
             34 LOAD_CONST               1 ('hello')
             36 LOAD_FAST                0 (n)
             38 CALL_FUNCTION            2
             40 POP_TOP
             42 JUMP_ABSOLUTE           28
...
__setitem__的使用涉及到一个函数调用(参见CALL_FUNCTIONPOP_TOP的使用,而不仅仅是STORE_SUBSCR——这是在底层的区别),并且函数调用会增加一定的开销,因此使用方括号访问符号会导致更优化的操作码。

那样就可以解释了。然而,在实现 __setitem__ 的自定义类的情况下,它是如何运作的呢?它也会更快吗?这似乎是反直觉的,但我并不真正熟悉所有这些。 - Phobia
2
也许?你需要运行一些测试,尽管使用方括号访问器生成的字节码仍将保持为“STORE_SUBSCR”,因为编译器不会进行区分。但是,考虑到最终将调用自定义的__setitem__,这可能导致额外的开销。 - metatoaster

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接