如何解决itertools 与用于展平和重复的生成器表达式 - 我们如何解释这些计时结果?
受到关于 this question 的讨论的启发,我决定尝试一些性能测试。我设置的任务稍微简单一些 - 给定一个源列表 A
,我们希望创建一个重复 A 的每个元素 N 次的惰性迭代:
def test(implementation):
A,N = list('abc'),3
assert list(implementation(A,N)) == list('aaabbbccc')
我想出了几个实现,并这样测试它们:
from itertools import chain,repeat,starmap
from timeit import timeit
flatten = chain.from_iterable
def consume(iterable):
for _ in iterable:
pass
# FAST approaches
def tools(original,count):
return flatten(map(repeat,original,repeat(count)))
def tools_star(original,count):
return flatten(starmap(repeat,zip(original,repeat(count))))
def mixed(original,count):
return flatten(repeat(a,count) for a in original)
# SLOW approaches
def mixed2(original,count):
return (x for a in original for x in repeat(a,count))
def explicit(original,count):
for a in original:
for _ in range(count):
yield a
def generator(original,count):
return (a for a in original for _ in range(count))
def mixed3(original,count):
return flatten((a for _ in range(count)) for a in original)
if __name__ == '__main__':
for impl in (tools,tools_star,mixed,mixed2,explicit,generator,mixed3):
for consumption in (consume,list):
to_time = lambda: consumption(impl(list(range(1000)),1000))
elapsed = timeit(to_time,number=100)
print(f'{consumption.__name__}({impl.__name__}): {elapsed:.2f}')
以下是我机器上计时结果的三个示例:
consume(tools): 1.10
list(tools): 2.96
consume(tools_star): 1.10
list(tools_star): 2.97
consume(mixed): 1.11
list(mixed): 2.91
consume(mixed2): 4.60
list(mixed2): 6.53
consume(explicit): 5.45
list(explicit): 8.09
consume(generator): 5.98
list(generator): 7.62
consume(mixed3): 5.75
list(mixed3): 7.67
consume(tools): 1.10
list(tools): 2.88
consume(tools_star): 1.10
list(tools_star): 2.89
consume(mixed): 1.11
list(mixed): 2.87
consume(mixed2): 4.56
list(mixed2): 6.39
consume(explicit): 5.42
list(explicit): 7.24
consume(generator): 5.91
list(generator): 7.48
consume(mixed3): 5.80
list(mixed3): 7.61
consume(tools): 1.14
list(tools): 2.98
consume(tools_star): 1.10
list(tools_star): 2.90
consume(mixed): 1.11
list(mixed): 2.92
consume(mixed2): 4.76
list(mixed2): 6.49
consume(explicit): 5.69
list(explicit): 7.38
consume(generator): 5.68
list(generator): 7.52
consume(mixed3): 5.75
list(mixed3): 7.86
由此我得出以下结论:
-
itertools
工具提供了巨大的性能提升,但前提是我们两者都使用它们来“展平”迭代器(itertools.chain.from_iterable
而不是通过嵌套for
表达式)and 以生成子序列(itertools.repeat
而不是range
)。仅使用repeat
只能提供很小的改进,而仅使用chain.from_iterable
实际上似乎会使事情变得更糟。 -
对于完整的
itertools
实现,我们如何迭代输入序列似乎并不重要 - 无论是使用生成器表达式、使用map
还是使用 {{1} }. (这并不奇怪,因为这里只发生 O(len(A)) 操作而不是 O(len(A) * N)。itertools.starmap
方法很笨拙,绝对不是我推荐的,但我包括因为原始激励讨论中的代码使用了它。) -
从可迭代对象创建列表所增加的开销似乎在方法和计时运行之间变化很大(请注意两次运行中
starmap
结果的差异) - 尽管它们看起来使快速方法更加一致。这尤其奇怪,因为我正在总结每个测试中多个列表创建的结果。
list(explicit)
的幕后到底发生了什么?我们如何解释这些时序结果?尤其奇怪的是 itertools
和 chain.from_iterable
在这里没有提供增量性能优势,而是完全相互依赖。列表构建是怎么回事?每种情况下增加的开销是否相同(重复将相同的元素序列附加到空列表)?
解决方法
它主要归结为花费在解释器上的时间量的大O。
- 解释器中没有循环允许 C 函数直接通信。
- 但是嵌套如此多的 itertools 会增加少量但可衡量的开销。
-
I
在下表中。
-
- 但是嵌套如此多的 itertools 会增加少量但可衡量的开销。
- 一个循环只是几个操作码的 ×1000。
- 嵌套循环高达 1000000 倍。
- 直接从
repeat
产生的操作码比在产生前从range
存储短一些操作码。-
Y
在下表中。
-
-
explicit
和generator
实际上是等价的。 - 嵌套生成器是一个嵌套函数调用——代价高昂。
-
G
在下表中。
-
这是我的结果:
方法 | 复杂性(仅限口译员) | 列表 | 消费 |
---|---|---|---|
工具 | O(0) + I |
0.21 | 0.09 |
tools_star | O(0) + I |
0.21 | 0.09 |
混合 | O(N) |
0.20 | 0.09 |
mixed2 | O(N²) |
0.54 | 0.47 |
显式 | O(N²) + Y |
0.64 | 0.60 |
发电机 | O(N²) + Y |
0.64 | 0.60 |
工具 | O(N²) + G |
0.71 | 0.65 |
看起来很有说服力。
我还对代码进行了一些修改,以提高计时方法和可读性。
PRODUCERS = (tools,tools_star,mixed,mixed2,explicit,generator,mixed3)
CONSUMERS = (list,consume)
N = 1000
SAMPLES = 50
BATCH = 10
if __name__ == '__main__':
for consumer in CONSUMERS:
print(f"{consumer.__name__}:")
for producer in PRODUCERS:
to_time = lambda: consumer(producer(list(range(N)),N))
elapsed = timeit.repeat(to_time,repeat=SAMPLES,number=BATCH)
emin = min(elapsed) # get rid of the random fluctuations for the best theoretical time
print(f'{emin:02.2f} | {producer.__name__}')
print()
版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。