为什么__getitem __(键)和get(键)明显慢于[key]?

时间:2012-05-30 20:18:39

标签: python profiling benchmarking

据我了解,括号只不过是__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}

这个问题是我的基准测试方式吗?我尝试用简单的“通道”替换括号访问,以确保实际访问数据,并发现“通过”运行得更快。我对此的解释是确实正在访问数据。我也尝试添加一个新的列表,它给出了类似的结果。

1 个答案:

答案 0 :(得分:9)

首先,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

现在,获得正确的基准测试对于阅读结果中的任何内容显然非常重要,而且我不知道在那里有多少帮助。但假设确实存在差异(这对我来说很有意义),这是我猜测的原因:

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

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

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