在过去的几天里,我一直在尝试更好地理解计算复杂性以及如何改进Python代码。为此,我尝试了不同的函数来计算斐波那契数,比较了如果我做了一些小的改动,脚本将运行多长时间。
我正在使用列表计算斐波那契数,将列表中元素-2和-1的总和相加。
我很困惑地发现,如果在循环中添加.pop(),删除列表中不需要的元素,我的脚本运行速度将大大提高。我不明白为什么会这样。循环中的每一步,计算机都会做另外一件事。因此,我未经训练的直觉表明这应该增加计算时间。当列表很长时,“查找”列表的最后一个元素会慢很多吗?
这是我的代码:
import time
import numpy as np
def fib_stack1(n):
""" Original function """
assert type(n) is int, 'Expected an integer as input.'
if n < 2:
return n
else:
stack = [0, 1]
for i in range(n-1):
stack.append(stack[-1] + stack[-2])
return stack[-1]
def fib_stack2(n):
""" Modified function """
assert type(n) is int, 'Expected an integer as input.'
if n < 2:
return n
else:
stack = [0, 1]
for i in range(n-1):
stack.append(stack[-1] + stack[-2])
### CHANGE ###
stack.pop(-3)
##############
return stack[-1]
rec1 = []
rec2 = []
for _ in range(10):
t1 = time.time()
fib_stack1(99999)
t2 = time.time()
rec1.append(t2-t1)
t1 = time.time()
fib_stack2(99999)
t2 = time.time()
rec2.append(t2-t1)
print(np.array(rec1).mean())
print(np.array(rec2).mean())
输出如下:
# Original
0.26878631115
# Modified
0.145034956932
当列表很长时,“查找”列表的最后一个元素会慢很多吗?
不,列表长度对查找速度没有影响。这些是数组列表,而不是链接列表。这很可能与内存分配或缓存性能有关。还涉及垃圾收集器。
当您删除不需要的列表元素时,Python永远不必为列表分配更大的缓冲区。它也可能能够重用为int
对象分配的内存,而不是从OS请求更多的内存。考虑到整数有多大,重用它们的内存很重要。(内存分配的详细信息取决于Python版本和基础标准库分配器。Python2拥有int
s的可用列表,而不是long
s; Python 3没有int
s的可用列表。Python本身不努力重用大型的分配对象,但基础分配器可能正在做某事。)
另外,当您必须继续分配新的整数,尤其是与第99999个斐波那契数一样大的整数时,您将不会从CPU的缓存中获得太多好处。主内存访问比缓存慢得多。
最后,您的分配模式fib_stack1
(大量分配,没有太多的对象引用计数降为0)触发了Python的循环检测器系统(又名垃圾收集器),该系统需要时间才能运行,并且会占用大量不需要触摸的内存,损害缓存性能。在我自己的测试中,特别是在Python 3上,暂时禁用收集器会产生明显的加速fib_stack1
。
本文收集自互联网,转载请注明来源。
如有侵权,请联系 [email protected] 删除。
我来说两句