为啥 __getitem__(key) 和 get(key) 比 [key] 慢很多?

Posted

技术标签:

【中文标题】为啥 __getitem__(key) 和 get(key) 比 [key] 慢很多?【英文标题】:Why are __getitem__(key) and get(key) significantly slower than [key]?为什么 __getitem__(key) 和 get(key) 比 [key] 慢很多? 【发布时间】:2012-05-30 20:18:39 【问题描述】:

据我了解,括号只不过是__getitem__ 的包装。以下是我对此进行基准测试的方法:

首先,我生成了一个半大字典。

items = 
for i in range(1000000):
    items[i] = 1

然后,我用cProfile测试了以下三个功能:

def get2(items):
    for k in items.iterkeys():
        items.get(k)

def magic3(items):
    for k in items.iterkeys():
        items.__getitem__(k)

def brackets1(items):
    for k in items.iterkeys():
        items[k]

结果如下:

         1000004 function calls in 3.779 CPU seconds

   Ordered by: standard name

   ncalls  tottime  percall  cumtime  percall filename:lineno(function)
        1    0.000    0.000    3.779    3.779 <string>:1(<module>)
        1    2.135    2.135    3.778    3.778 dict_get_items.py:15(get2)
        1    0.000    0.000    0.000    0.000 method 'disable' of '_lsprof.Profiler' objects
  1000000    1.644    0.000    1.644    0.000 method 'get' of 'dict' objects
        1    0.000    0.000    0.000    0.000 method 'iterkeys' of 'dict' objects


         1000004 function calls in 3.679 CPU seconds

   Ordered by: standard name

   ncalls  tottime  percall  cumtime  percall filename:lineno(function)
        1    0.000    0.000    3.679    3.679 <string>:1(<module>)
        1    2.083    2.083    3.679    3.679 dict_get_items.py:19(magic3)
  1000000    1.596    0.000    1.596    0.000 method '__getitem__' of 'dict' objects
        1    0.000    0.000    0.000    0.000 method 'disable' of '_lsprof.Profiler' objects
        1    0.000    0.000    0.000    0.000 method 'iterkeys' of 'dict' objects


         4 function calls in 0.136 CPU seconds

   Ordered by: standard name

   ncalls  tottime  percall  cumtime  percall filename:lineno(function)
        1    0.000    0.000    0.136    0.136 <string>:1(<module>)
        1    0.136    0.136    0.136    0.136 dict_get_items.py:11(brackets1)
        1    0.000    0.000    0.000    0.000 method 'disable' of '_lsprof.Profiler' objects
        1    0.000    0.000    0.000    0.000 method 'iterkeys' of 'dict' objects

问题出在我进行基准测试的方式上吗?我尝试用一​​个简单的“pass”替换括号访问,以确保实际访问数据,并发现“pass”运行得更快。我对此的解释是数据确实被访问了。我还尝试附加到一个新列表,结果相似。

【问题讨论】:

我的猜测(这只是一个猜测)是[] 不会创建新的堆栈级别,而另一个会创建。有趣。 对于像这样的微基准测试,timeit 更好,因为它非常精确,消除了几个陷阱,并且自动循环语句以获得足够的数据而不会花费太长时间。我怀疑它对结果的改变很大,尽管放缓可能不那么严重。编辑@Not_a_Golfer:这三个方法都应该是C代码,而且C栈帧很便宜。 也许括号使用惰性求值?尝试对所有结果进行汇总以确保它们被使用。 这是我对dis 的实验。对我来说似乎是一个足够好的理由:pastebin.com/UzHGbSmg 我们可能不得不去挖掘解释器源代码。我相信 builtin_function_or_method 有一个特殊情况,因为为它们创建 Python 框架没有任何意义。 @Not_a_Golfer 我也猜到了,但是 OP 报告的差异很大。也许这只是由于一个糟糕的分析方法,数字timeit 给了我适合添加一些操作码。 【参考方案1】:

首先是Not_a_Golfer贴的拆解:

>>> d = 1:2
>>> dis.dis(lambda: d[1])
  1           0 LOAD_GLOBAL              0 (d)
              3 LOAD_CONST               1 (1)
              6 BINARY_SUBSCR       
              7 RETURN_VALUE   

>>> dis.dis(lambda: d.get(1))
  1           0 LOAD_GLOBAL              0 (d)
              3 LOAD_ATTR                1 (get)
              6 LOAD_CONST               1 (1)
              9 CALL_FUNCTION            1
             12 RETURN_VALUE  

>>> dis.dis(lambda: d.__getitem__(1))
  1           0 LOAD_GLOBAL              0 (d)
              3 LOAD_ATTR                1 (__getitem__)
              6 LOAD_CONST               1 (1)
              9 CALL_FUNCTION            1
             12 RETURN_VALUE

现在,正确进行基准测试对于将任何内容读入结果显然很重要,而我知道的还不够多,无法提供太多帮助。但是假设确实存在差异(这对我来说很有意义),这是我对为什么存在的猜测:

    dict.get 只是“做得更多”;它必须检查密钥是否存在,如果不存在则返回其第二个参数(默认为None)。这意味着存在某种形式的条件或异常捕获,所以我完全不惊讶这与检索与键关联的值的更基本操作具有不同的时序特征。

    Python 有一个用于“订阅”操作的特定字节码(如反汇编所示)。内置类型,包括dict,主要是在 C 中实现的,它们的实现不一定遵循正常的 Python 规则(只需要它们的接口,即使在那里也有很多极端情况)。所以我的猜测是,BINARY_SUBSCR 操作码的实现或多或少直接到支持此操作的内置类型的底层 C 实现。对于这些类型,我希望它实际上是 __getitem__ 作为 Python 级方法存在来包装 C 实现,而不是括号语法调用 Python 级方法。

对于实现__getitem__ 的自定义类的实例,将thing.__getitem__(key)thing[key] 进行基准测试可能会很有趣;实际上,您可能会在那里看到相反的结果,因为 BINARY_SUBSCR 操作码在内部必须退回到执行等效的工作来查找方法并调用它。

【讨论】:

在提供相同功能的同时,[]+异常处理是否比.get()更快? 是的,带有异常处理的方法是最快的。我得到了以下结果来获得 2000 万个条目: get() : 7.57s 检查 dict 中的键,然后使用括号:7.48s 使用括号和 try/except:5.42s 而不检查键是否存在:4.68跨度> @ChrisKoston,你产生了多少异常? try/except 的性能是不对称的;当没有抛出异常时,它很快,但是当抛出异常时,它很慢。您应该对更多查找失败进行相同的测试。

以上是关于为啥 __getitem__(key) 和 get(key) 比 [key] 慢很多?的主要内容,如果未能解决你的问题,请参考以下文章

__getitem__\__setitem__\__delitem__

python-面向对象之item系列(__getitem__,__setitem__,__delitem__)

为啥当我使用 [:] 时我的子类的 __getitem__ 和 __setitem__ 没有被调用?

面向对象-内置方法

带有“__getitem__”的类没有“get”方法

python 魔法方法之:__getitem__ __setitem__ __delitem__