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

时间:2014-02-12 17:05:46

标签: 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)

直观地说,我预计第二个实现会稍快一些,因为它可能会跳过从括号表示法转换为函数调用的步骤。但是,这并不是我的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'

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

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

正如您所看到的,使用self.__getitem__比<{1}} 更快 ,但self[key]显着更慢而不是self.__setitem__ {1}}。这看起来很奇怪 - 我知道函数调用开销可能很大,但如果这是问题我希望在两种情况下看到括号表示法更快,这在这里没有发生。


我进一步调查了一下;以下是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 指令可能会有很大的开销,但它真的可以 吗?这是唯一在这里脱颖而出的东西......任何人都可以看到正在发生的事情POP_TOP相对于__setitem__而言比它的表兄弟慢得多吗?

潜在相关信息:

win32

CPython 3.3.2 32位

1 个答案:

答案 0 :(得分: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}
"""

setitemgetitem都比相应的setbrackgetbrack慢:

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

但是,如果我完全按照您的测试运行,那么我会得到相同的结果 - 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'''

然后我再次得到括号总是比方法更快的行为。那么它是否与self[]子类中的dict如何工作有关?