性能差异:obj .__ setitem __(x,y)与obj [x] = y?

Hen*_*ter 7 python performance

我正在编写一个dict带属性访问的简单子类,当我进行优化时,我偶然发现了一些看起来很奇怪的东西.我原本写的__getattr____setattr__方法的简单别名self[key]等,但后来我想它应该是更快地打电话self.__getitem__self.__setitem__直接,因为他们大概会在引擎盖下被称为[key]符号.出于好奇,我计算了两个实现,并发现了一些惊喜.

以下是两种实现方式:正如您所看到的,这里没有太多内容.

# brackets
class AttrDict(dict):
    def __getattr__(self, key):
        return self[key]
    def __setattr__(self, key, val):
        self[key] = val

# methods
class AttrDict(dict):
    def __getattr__(self, key):
        return self.__getitem__(key)
    def __setattr__(self, key, val):
        self.__setitem__(key, val)
Run Code Online (Sandbox Code Playgroud)

直觉上,我预计第二个实现会稍快一些,因为它可能会跳过从括号表示法转换为函数调用的步骤.但是,这并不是我的timeit结果所显示的.

>>> methods = '''\
... class AttrDict(dict):
...     def __getattr__(self, key):
...         return self.__getitem__(key)
...     def __setattr__(self, key, val):
...         self.__setitem__(key, val)
... o = AttrDict()
... o.att = 1
... '''
>>> brackets = '''\
... class AttrDict(dict):
...     def __getattr__(self, key):
...         return self[key]
...     def __setattr__(self, key, val):
...         self[key] = val
...
... o = AttrDict()
... o.att = 1
... '''
>>> getting = 'foo = o.att'
>>> setting = 'o.att = 1'
Run Code Online (Sandbox Code Playgroud)

上面的代码都只是设置.以下是测试:

>>> for op in (getting, setting):
...     print('GET' if op == getting else 'SET')
...     for setup in (brackets, methods):
...             s = 'Brackets:' if setup == brackets else 'Methods:'
...             print(s, min(timeit.repeat(op, setup, number=1000000, repeat=20)))
...
GET
Brackets: 1.109725879526195
Methods: 1.050940903987339
SET
Brackets: 0.44571820606051915
Methods: 0.7166479863124096
>>>
Run Code Online (Sandbox Code Playgroud)

正如你所看到的,使用self.__getitem__是非常轻微self[key],但self.__setitem__就是显著self[key] = val.这看起来很奇怪 - 我知道函数调用开销可能很大,但如果这是问题,我希望在两种情况下看到括号表示法更快,这在这里没有发生.


我进一步调查了一下; 这里的dis结果:

>>> exec(brackets)
>>> dis.dis(AttrDict.__getattr__)
  3           0 LOAD_FAST                0 (self)
              3 LOAD_FAST                1 (key)
              6 BINARY_SUBSCR
              7 RETURN_VALUE
>>> dis.dis(AttrDict.__setattr__)
  5           0 LOAD_FAST                2 (val)
              3 LOAD_FAST                0 (self)
              6 LOAD_FAST                1 (key)
              9 STORE_SUBSCR
             10 LOAD_CONST               0 (None)
             13 RETURN_VALUE
>>> exec(methods)
>>> dis.dis(AttrDict.__getattr__)
  3           0 LOAD_FAST                0 (self)
              3 LOAD_ATTR                0 (__getitem__)
              6 LOAD_FAST                1 (key)
              9 CALL_FUNCTION            1 (1 positional, 0 keyword pair)
             12 RETURN_VALUE
>>> dis.dis(AttrDict.__setattr__)
  5           0 LOAD_FAST                0 (self)
              3 LOAD_ATTR                0 (__setitem__)
              6 LOAD_FAST                1 (key)
              9 LOAD_FAST                2 (val)
             12 CALL_FUNCTION            2 (2 positional, 0 keyword pair)
             15 POP_TOP
             16 LOAD_CONST               0 (None)
             19 RETURN_VALUE
Run Code Online (Sandbox Code Playgroud)

我唯一能想到的是,POP_TOP与其他调用相比,这条指令可能会有很大的开销,但它真的可以那么多吗?这是唯一在这里脱颖而出的东西......任何人都可以看到正在发生的事情__setitem__比它的支架表兄慢得多,相对于__getitem__

潜在相关信息:

win32上使用CPython 3.3.2 32位

Cla*_*diu 4

嗯,这很有趣。如果我运行你的东西的精简版本:

setup="""
def getbrack(a, b):
    return a[b]

def getitem(a, b):
    return a.__getitem__(b)

def setbrack(a, b, c):
    a[b] = c

def setitem(a, b, c):
    return a.__setitem__(b, c)

a = {2: 3}
"""
Run Code Online (Sandbox Code Playgroud)

setitemgetitem都比相应的setbrack和慢getbrack

>>> timeit.timeit("getbrack(a, 2)", setup, number=10000000)
1.1424450874328613
>>> timeit.timeit("getitem(a, 2)", setup, number=10000000)
1.5957350730895996
>>> timeit.timeit("setbrack(a, 2, 3)", setup, number=10000000)
1.4236340522766113
>>> timeit.timeit("setitem(a, 2, 3)", setup, number=10000000)
2.402789831161499
Run Code Online (Sandbox Code Playgroud)

但是,如果我完全运行您的测试,那么我会得到与您相同的结果 -GET 'Brackets'GET 'Methods'.

这意味着它与您正在使用的类有关,而不是括号与 setitem 本身。


现在,如果我修改测试以不再引用self......

brackets = '''d = {}

class AttrDict2(dict):
    def __getattr__(self, key):
        return d[key]
    def __setattr__(self, key, val):
        d[key] = val

o = AttrDict2()
o.att = 1'''

methods = '''d = {}

class AttrDict2(dict):
    def __getattr__(self, key):
        return d.__getitem__(key)
    def __setattr__(self, key, val):
        d.__setitem__(key, val)

o = AttrDict2()
o.att = 1'''
Run Code Online (Sandbox Code Playgroud)

然后我再次得到括号总是比方法更快的行为。self[]那么也许这与子类中的工作方式有关dict