Warning: file_get_contents(/data/phpspider/zhask/data//catemap/2/python/311.json): failed to open stream: No such file or directory in /data/phpspider/zhask/libs/function.php on line 167

Warning: Invalid argument supplied for foreach() in /data/phpspider/zhask/libs/tag.function.php on line 1116

Notice: Undefined index: in /data/phpspider/zhask/libs/function.php on line 180

Warning: array_chunk() expects parameter 1 to be array, null given in /data/phpspider/zhask/libs/function.php on line 181
Python timeit和它的默认\u计时器完全不一致_Python_Performance - Fatal编程技术网

Python timeit和它的默认\u计时器完全不一致

Python timeit和它的默认\u计时器完全不一致,python,performance,Python,Performance,我对这两个函数进行了基准测试(它们将对解压回源代码列表,来自): 使用timeit.timeit的结果(五轮,数字为秒): 所以很明显,f1比f2快得多,对吗 但是我也用timeit.default\u timer进行了测量,得到了完全不同的图片: f1 7.28 f2 1.92 f1 5.34 f2 1.66 f1 6.46 f2 1.70 f1 6.82 f2 1.59 f1 5.88 f2 1.63 很明显,f2要快得多,对吗 唉。为什么

我对这两个函数进行了基准测试(它们将对解压回源代码列表,来自):

使用
timeit.timeit
的结果(五轮,数字为秒):

所以很明显,
f1
f2
快得多,对吗

但是我也用
timeit.default\u timer
进行了测量,得到了完全不同的图片:

f1 7.28   f2 1.92   
f1 5.34   f2 1.66   
f1 6.46   f2 1.70   
f1 6.82   f2 1.59   
f1 5.88   f2 1.63   
很明显,
f2
要快得多,对吗

唉。为什么计时完全不同,我应该相信哪种计时方法

完整基准代码:

from timeit import timeit, default_timer

n = 10**7
a = list(range(n))
b = list(range(n))
pairs = list(zip(a, b))

def f1(a, b, pairs):
    a[:], b[:] = zip(*pairs)

def f2(a, b, pairs):
    for i, (a[i], b[i]) in enumerate(pairs):
        pass

print('timeit')
for _ in range(5):
    for f in f1, f2:
        t = timeit(lambda: f(a, b, pairs), number=1)
        print(f.__name__, '%.2f' % t, end='   ')
    print()

print('default_timer')
for _ in range(5):
    for f in f1, f2:
        t0 = default_timer()
        f(a, b, pairs)
        t = default_timer() - t0
        print(f.__name__, '%.2f' % t, end='   ')
    print()

正如Martijn所评论的,区别在于Python的垃圾收集,它在运行期间禁用了
timeit.timeit
。和
zip
,每1000万个iterables一个

所以,垃圾收集1000万个对象只是需要很多时间,对吗?谜团解开了

嗯。。。不,事实并非如此,而且比这有趣多了。在现实生活中,有一个教训可以让这样的代码更快

Python丢弃不再需要的对象的主要方法是引用计数。此处禁用的垃圾收集器用于引用循环,引用计数无法捕获这些循环。这里没有任何循环,所以它通过引用计数被丢弃,垃圾收集器实际上不收集任何垃圾

让我们看几件事。首先,让我们自己禁用垃圾回收器来重现更快的时间

通用设置代码(所有其他代码块应在此之后以新的运行方式直接运行,不要将它们组合在一起):

启用垃圾收集的计时(默认值):

我跑了三次,分别花了7.09秒、7.03秒和7.09秒

禁用垃圾收集的计时:

分别用了0.96秒、1.02秒和0.99秒

所以现在我们知道垃圾收集确实占用了大部分时间,尽管它没有收集任何东西

这里有一些有趣的事情:大部分时间都是由
zip
迭代器的创建造成的:

t0 = timer()
z = zip(*pairs)
t1 = timer()
print(t1 - t0)
这花了6.52秒、6.51秒和6.50秒

请注意,我将
zip
迭代器保存在一个变量中,因此即使通过引用计数或垃圾收集,也没有任何东西可以丢弃

什么?!那么,时间到哪里去了

嗯。。。正如我所说,没有引用循环,所以垃圾收集器实际上不会收集任何垃圾。但是垃圾收集器不知道!为了弄清楚这一点,它需要检查

因为迭代器可能成为引用周期的一部分,所以它们被注册用于垃圾收集跟踪。让我们看看由于
zip
创建(在公共设置代码之后执行此操作),还有多少对象被跟踪:

输出:
10000003
跟踪新对象。我相信这就是
zip
对象本身,它的内部元组用来存放迭代器,它的内部元组,以及1000万个迭代器

好的,垃圾收集器跟踪所有这些对象。但这意味着什么?好的,每隔一段时间,在创建了一定数量的新对象之后,收集器会检查跟踪的对象,看看是否有一些是垃圾,可以丢弃。收集器保留三代跟踪对象。新对象将进入第0代。如果他们在那里的一次收集中幸存下来,他们将进入第一代。如果他们在那里的一个收藏中幸存下来,他们将被转移到第二代。如果它们能在那里继续收集,它们将留在第二代。让我们检查一下之前和之后的几代人:

gc.collect()
print('collections:', [stats['collections'] for stats in gc.get_stats()])
print('objects:', [len(gc.get_objects(i)) for i in range(3)])
z = zip(*pairs)
print('collections:', [stats['collections'] for stats in gc.get_stats()])
print('objects:', [len(gc.get_objects(i)) for i in range(3)])
输出(每行显示三代的值):

10011140表明,1000万个迭代器中的大多数不仅注册用于跟踪,而且已经在第2代中。因此,它们至少是两次垃圾收集运行的一部分。第2代收集的数量从2增加到了20,因此我们数以百万计的迭代器是多达20次垃圾收集运行的一部分(两次进入第2代,而在第2代中已经有18次)。我们还可以注册回调以更精确地计数:

checks = 0
def count(phase, info):
    if phase == 'start':
        global checks
        checks += len(gc.get_objects(info['generation']))

gc.callbacks.append(count)
z = zip(*pairs)
gc.callbacks.remove(count)
print(checks)
这告诉我总共有63891314次检查(即,平均而言,每个迭代器都是6次以上垃圾收集运行的一部分)。这是一个很大的工作。所有这些只是为了在使用它之前创建
zip
迭代器

与此同时,循环

for i, (a[i], b[i]) in enumerate(pairs):
    pass
几乎不创建任何新对象。让我们检查一下跟踪
枚举的原因有多少:

gc.collect()
tracked_before = len(gc.get_objects())
e = enumerate(pairs)
print(len(gc.get_objects()) - tracked_before)
输出:
3
跟踪的新对象(
枚举
迭代器对象本身,它为迭代
对而创建的单个迭代器,以及它将使用的结果元组(code))

我认为这回答了“为什么时间安排完全不同?”。
zip
解决方案创建数百万个经过多次垃圾收集运行的对象,而循环解决方案没有。因此,禁用垃圾收集器可以极大地帮助
zip
解决方案,而循环解决方案并不在意

关于第二个问题:“我应该相信哪种计时方法?”。以下是关于这件事(我的重点):

默认情况下,
timeit()
在计时期间临时关闭垃圾收集。这种方法的优点是,它使独立计时更具可比性。缺点是,GC可能是被测函数性能的重要组成部分。如果是这样,GC可以作为设置字符串中的第一条语句重新启用。例如:

timeit.Timer('for i in range(10): oct(i)', 'gc.enable()').timeit()
在我们这里的例子中,垃圾收集的成本并不是源于其他一些不相关的代码。这是由
zip
调用直接导致的。事实上,当你运行它时,你确实付出了这个代价。因此,在这种情况下,我认为它是F的性能的重要组成部分。
gc.collect()
tracked_before = len(gc.get_objects())
z = zip(*pairs)
print(len(gc.get_objects()) - tracked_before)
gc.collect()
print('collections:', [stats['collections'] for stats in gc.get_stats()])
print('objects:', [len(gc.get_objects(i)) for i in range(3)])
z = zip(*pairs)
print('collections:', [stats['collections'] for stats in gc.get_stats()])
print('objects:', [len(gc.get_objects(i)) for i in range(3)])
collections: [13111, 1191, 2]
objects: [17, 0, 13540]
collections: [26171, 2378, 20]
objects: [317, 2103, 10011140]
checks = 0
def count(phase, info):
    if phase == 'start':
        global checks
        checks += len(gc.get_objects(info['generation']))

gc.callbacks.append(count)
z = zip(*pairs)
gc.callbacks.remove(count)
print(checks)
for i, (a[i], b[i]) in enumerate(pairs):
    pass
gc.collect()
tracked_before = len(gc.get_objects())
e = enumerate(pairs)
print(len(gc.get_objects()) - tracked_before)
timeit.Timer('for i in range(10): oct(i)', 'gc.enable()').timeit()
def f1(a, b, pairs):
    gc.disable()
    a[:], b[:] = zip(*pairs)
    gc.enable()