Is dict.__setitem__(key, x) slower (or faster) than dict[key] = x, and why?

Viewed 61

I found out something weird.

I defined two test functions as such:

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)

One would expect the two functions to be roughly the same execution speed. However:

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

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

There is possibly something I missed, but it does seem like setitem is almost twice as long, and I don't really understand why. Isn't dict[key] = x supposed to call __setitem__?

(Using CPython 3.9)

Edit: Using timeit instead of time

1 Answers

Isn't dict[key] = x supposed to call __setitem__?

Strictly speaking, no. Running both your functions through dis.dis, we get (I am only including the for loop):

>>> 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
...

The usage of __setitem__ involves a function call (see the usage of CALL_FUNCTION and POP_TOP instead of just STORE_SUBSCR - that's the difference underneath the hood), and function calls do add some amount of overhead, so using the bracket accessor leads to more optimised opcode.

Related