过去几天我一直在努力更好地理解计算复杂性以及如何改进Python代码。为此,我尝试了不同的函数来计算Fibonacci数,比较如果我进行小的更改,脚本运行的时间。
我正在使用列表计算斐波那契数字,从列表中添加元素-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
答案 0 :(得分:6)
list
以连续方式将其元素存储在内存中。
因此append
对象的list
方法需要不时地调整分配的内存块(不是每次都调用append
),幸运的是)
有时,系统可以调整原位&#34;就地&#34; (在当前内存块之后分配更多内存),有时不会:它必须找到一个足够大的内存块来存储新列表。
当调整大小不是&#34;就地&#34;时,需要复制现有数据。 (请注意,当列表的大小减少时,不会发生这种情况)
因此,如果列表中复制的元素较少,则操作会更快。
请注意list.append
仍然非常快。在列表末尾添加是最快的方式(与insert
相比,每次必须移动元素以释放其#34;插槽&#34;)
答案 1 :(得分:6)
当列表很长时,“查找”列表的最后一个元素会慢得多吗?
不,列表长度对查找速度没有影响。这些是arraylists,而不是链接列表。这更可能与内存分配或缓存性能有关。垃圾收集器也参与其中。
当您删除不需要的列表元素时,Python永远不必为列表分配更大的缓冲区。它也可以重用为A = np.empty( (len(files), 100, 100) )
对象分配的内存,而不是从OS请求更多内存。考虑到你的整数有多大,重用他们的记忆是一件大事。 (内存分配的细节取决于Python版本和底层标准库分配器.Python 2有int
的免费列表,但不是int
s; Python 3没有{{{的免费列表1}} s。Python本身不会为大对象重用分配,但底层分配器可能正在做某事。)
此外,当你必须继续分配新的整数时,特别是那些像99999th斐波纳契数一样大的整数,你不会从CPU的缓存中获得太多的好处。主内存访问比缓存慢得多。
最后,你的long
的分配模式(大量的分配,而不是那么多的对象引用计数降到0)会触发Python的循环检测器系统,也就是垃圾收集器,这需要时间来运行和接触很多内存不需要触摸,损害缓存性能。暂时disabling the collector在我自己的测试中为int
产生了显着的加速,特别是在Python 3上。
答案 2 :(得分:3)
不,查找列表中的任何元素都是在相同的时间内完成的(计算机科学中所谓的恒定时间行为)。将调用添加到pop
确实会增加每次循环迭代所需的工作量,但列表永远不会超过3个元素。在您的第一个版本中,列表会在每次迭代中增长,并且此类操作可以完全免费或非常昂贵,具体取决于附加 >列表实际上已分配的内存,这是一种无法直接访问的信息。
基本上,当您实例化一个列表时,会预先分配一些额外的空间,为将来的append
腾出空间而牺牲&#34;浪费&#34;空间。如果列表被填满,则需要进一步扩大以进一步append
s发生,因此这些特定的附加要比通常更昂贵。如果数组末尾的内存中已经存在其他一些数据,则必须将列表元素中的所有数据(实际上只是指针)复制到新的内存位置,新列表可以存储在一个连续的内存块中。
有关列表增长行为的更多信息(仅在CPython中,因为这是特定于实现的),请参阅例如here