Python 中的函数查找操作的时间复杂度是多少
What is the time complexity of a function lookup operation in Python
我想知道,由于一种常见的优化策略是 "cache" 在变量中查找,然后使用该变量调用 method/function,查找操作的开销有多大?
这就是我所说的 "caching" 查找的意思,以防它不是正确的术语:
class TestClass:
def myMethod(self):
printMethod = self.printMethod
for i in range(0, 1000):
printMethod(i)
def printMethod(self, i):
print i
节省的不是时间复杂度,而是实际时间。在命名空间中查找函数名只是在字典中查找键,这已经是 O(1)。查找对象的属性也是字典查找,同样是 O(1)。有一个用于按名称查找局部变量的优化操作码,但仍然不能比 O(1) 更快。
在您的示例中,查找 self.printMethod
查找本地 (self
),然后查找属性 (printMethod
)。那是两次查找。如果将它存储在本地,那么对局部变量 printMethod
的每次后续访问都只是一次查找,而不是两次。那仍然是 O(1),但它更快,因为它是一个更小的常量。
This question 进一步讨论了名称查找在 Python 中的工作原理。
您可以使用以下代码计算时差:
以及一些计时结果:
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
所以一般缓存方法更快,方法查找时间取决于class继承层次结构的深度。
但请注意,如果您使用像 pypy 这样的跟踪 JIT,您可能会得到不同的结果,因为跟踪可能会有效地为您缓存方法指针。
两个 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
操作,从本地栈帧中的固定数字offest加载一个值,而访问self.printMethod
则需要额外的LOAD_ATTR
操作。
当然,建立局部变量的值确实需要时间,因此必须多次使用它(就像在您的代码示例中一样)才能看到任何性能优势。
正如@user5402 指出的那样,由于编译器方面的优化,您的里程数可能会因您使用的实现而异。
我想知道,由于一种常见的优化策略是 "cache" 在变量中查找,然后使用该变量调用 method/function,查找操作的开销有多大?
这就是我所说的 "caching" 查找的意思,以防它不是正确的术语:
class TestClass:
def myMethod(self):
printMethod = self.printMethod
for i in range(0, 1000):
printMethod(i)
def printMethod(self, i):
print i
节省的不是时间复杂度,而是实际时间。在命名空间中查找函数名只是在字典中查找键,这已经是 O(1)。查找对象的属性也是字典查找,同样是 O(1)。有一个用于按名称查找局部变量的优化操作码,但仍然不能比 O(1) 更快。
在您的示例中,查找 self.printMethod
查找本地 (self
),然后查找属性 (printMethod
)。那是两次查找。如果将它存储在本地,那么对局部变量 printMethod
的每次后续访问都只是一次查找,而不是两次。那仍然是 O(1),但它更快,因为它是一个更小的常量。
This question 进一步讨论了名称查找在 Python 中的工作原理。
您可以使用以下代码计算时差:
以及一些计时结果:
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
所以一般缓存方法更快,方法查找时间取决于class继承层次结构的深度。
但请注意,如果您使用像 pypy 这样的跟踪 JIT,您可能会得到不同的结果,因为跟踪可能会有效地为您缓存方法指针。
两个 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
操作,从本地栈帧中的固定数字offest加载一个值,而访问self.printMethod
则需要额外的LOAD_ATTR
操作。
当然,建立局部变量的值确实需要时间,因此必须多次使用它(就像在您的代码示例中一样)才能看到任何性能优势。
正如@user5402 指出的那样,由于编译器方面的优化,您的里程数可能会因您使用的实现而异。