【问题标题】:What is the time complexity of a function lookup operation in PythonPython中函数查找操作的时间复杂度是多少
【发布时间】:2015-03-03 18:09:43
【问题描述】:

我想知道,由于一种常见的优化策略是将查找“缓存”在变量中,然后使用该变量调用方法/函数,查找操作的成本是多少?

这就是我所说的“缓存”查找的意思,以防它不是正确的术语:

class TestClass:

    def myMethod(self):
       printMethod = self.printMethod
       for i in range(0, 1000):
          printMethod(i)

    def printMethod(self, i):
       print i

【问题讨论】:

  • 您是在问这种方法在以这种方式缓存或不以这种方式缓存时查找它的成本是多少?
  • 在大 O 术语中,它是 O(1)。这是你想知道的吗?
  • 我认为它们具有不同的时间复杂度,因为两种方法的计时方式不同。

标签: python time-complexity lookup


【解决方案1】:

节省的时间不是时间复杂度,而是实际时间。在命名空间中查找函数名称只是在字典中查找键,这已经是 O(1)。在对象上查找属性也是一个字典查找,也是 O(1)。有一个优化的操作码用于按名称查找局部变量,但仍然不能比 O(1) 快。

在您的示例中,查找 self.printMethod 查找本地 (self),然后查找属性 (printMethod)。这是两个查找。如果将其存储在本地,那么每次对局部变量 printMethod 的后续访问只是一次查找而不是两次查找。这仍然是 O(1),但它更快,因为它是一个更小的常数。

This question 进一步讨论了名称查找在 Python 中的工作方式。

【讨论】:

    【解决方案2】:

    这里有一些代码可以用来计时:

    http://pastebin.com/svBN5NZ9

    还有一些计时结果:

    In [2]: %timeit Class1().runCached(10000)
    1000 loops, best of 3: 1.74 ms per loop
    
    In [3]: %timeit Class1().runNormal(10000)
    100 loops, best of 3: 2.92 ms per loop
    
    In [4]: %timeit Class10().runCached(10000)
    1000 loops, best of 3: 1.7 ms per loop
    
    In [5]: %timeit Class10().runNormal(10000)
    100 loops, best of 3: 6.01 ms per loop
    
    In [6]: %timeit Class100().runCached(10000)
    1000 loops, best of 3: 1.7 ms per loop
    
    In [7]: %timeit Class100().runNormal(10000)
    10 loops, best of 3: 42.9 ms per loop
    

    所以通常缓存方法更快,方法查找时间取决于类继承层次结构的深度。

    但请注意,如果您使用像 pypy 这样的跟踪 JIT,您可能会得到不同的结果,因为跟踪可能会有效地为您缓存方法指针。

    【讨论】:

      【解决方案3】:

      两个 O(1) 操作可能需要非常不同的时间。实例属性查找(self.printMethod)和局部变量都是 O(1),但是局部变量访问经过优化,不需要字典查找,因此更快。查看字节码以访问局部变量vs CPython 中的实例变量:

      >>> import dis
      >>> class MyClass:
      ...   def printMethod(self):
      ...     pass
      ...   def code(self):
      ...     pm = self.printMethod
      ...     self.printMethod()
      ...     pm()
      ...
      >>> dis.dis(MyClass.code)
        5           0 LOAD_FAST                0 (self)
                    3 LOAD_ATTR                0 (printMethod)
                    6 STORE_FAST               1 (pm)
      
        6           9 LOAD_FAST                0 (self)
                   12 LOAD_ATTR                0 (printMethod)
                   15 CALL_FUNCTION            0
                   18 POP_TOP
      
        7          19 LOAD_FAST                1 (pm)
                   22 CALL_FUNCTION            0
                   25 POP_TOP
                   26 LOAD_CONST               0 (None)
                   29 RETURN_VALUE
      >>>
      

      您可以看到访问pm 需要一个简单的LOAD_FAST 操作,该操作从本地堆栈帧中的固定数值偏移中加载一个值,而访问self.printMethod 需要一个额外的LOAD_ATTR 操作。

      当然,确定局部变量的值确实需要时间,因此必须多次使用它(就像在您的代码示例中一样)才能看到任何性能优势。

      正如@user5402 指出的那样,由于编译器方面的优化,您的里程可能会因您使用的实现而异。

      【讨论】:

        猜你喜欢
        • 2019-07-15
        • 1970-01-01
        • 1970-01-01
        • 2012-11-27
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2017-02-01
        • 2011-02-16
        相关资源
        最近更新 更多