实际上有一些差异会导致观察到的性能差异。我的目标是对这些差异进行高级概述,但尽量不要过多介绍低级细节或可能的改进。对于基准测试,我使用我自己的包simple_benchmark。
生成器与 for 循环
生成器和生成器表达式是可以用来代替编写迭代器类的语法糖。
当你写一个生成器时:
def count_even(num):
s = str(num)
for c in s:
yield c in '02468'
或者一个生成器表达式:
(c in '02468' for c in str(num))
这将被(在幕后)转换为可通过迭代器类访问的状态机。最后它大致相当于(尽管围绕生成器生成的实际代码会更快):
class Count:
def __init__(self, num):
self.str_num = iter(str(num))
def __iter__(self):
return self
def __next__(self):
c = next(self.str_num)
return c in '02468'
所以生成器总是会有一个额外的间接层。这意味着推进生成器(或生成器表达式或迭代器)意味着您在生成器生成的迭代器上调用__next__,生成器本身在您实际要迭代的对象上调用__next__。但它也有一些开销,因为您实际上需要创建一个额外的“迭代器实例”。通常,如果您在每次迭代中做任何实质性的事情,这些开销可以忽略不计。
仅举例说明生成器与手动循环相比会产生多少开销:
import matplotlib.pyplot as plt
from simple_benchmark import BenchmarkBuilder
%matplotlib notebook
bench = BenchmarkBuilder()
@bench.add_function()
def iteration(it):
for i in it:
pass
@bench.add_function()
def generator(it):
it = (item for item in it)
for i in it:
pass
@bench.add_arguments()
def argument_provider():
for i in range(2, 15):
size = 2**i
yield size, [1 for _ in range(size)]
plt.figure()
result = bench.run()
result.plot()
生成器与列表推导式
生成器的优点是它们不创建列表,它们一个接一个地“生成”值。因此,虽然生成器具有“迭代器类”的开销,但它可以节省内存以创建中间列表。这是速度(列表理解)和内存(生成器)之间的权衡。这已经在 StackOverflow 周围的各种帖子中讨论过,所以我不想在这里详细介绍。
import matplotlib.pyplot as plt
from simple_benchmark import BenchmarkBuilder
%matplotlib notebook
bench = BenchmarkBuilder()
@bench.add_function()
def generator_expression(it):
it = (item for item in it)
for i in it:
pass
@bench.add_function()
def list_comprehension(it):
it = [item for item in it]
for i in it:
pass
@bench.add_arguments('size')
def argument_provider():
for i in range(2, 15):
size = 2**i
yield size, list(range(size))
plt.figure()
result = bench.run()
result.plot()
sum 应该比手动迭代更快
是的,sum 确实比显式 for 循环快。尤其是在迭代整数时。
import matplotlib.pyplot as plt
from simple_benchmark import BenchmarkBuilder
%matplotlib notebook
bench = BenchmarkBuilder()
@bench.add_function()
def my_sum(it):
sum_ = 0
for i in it:
sum_ += i
return sum_
bench.add_function()(sum)
@bench.add_arguments()
def argument_provider():
for i in range(2, 15):
size = 2**i
yield size, [1 for _ in range(size)]
plt.figure()
result = bench.run()
result.plot()
字符串方法与任何类型的 Python 循环
要了解使用 str.count 等字符串方法与循环(显式或隐式)相比的性能差异,Python 中的字符串实际上是作为 值 存储在(内部)数组中的。这意味着循环实际上不会调用任何__next__ 方法,它可以直接在数组上使用循环,这将显着更快。然而,它也会对字符串进行方法查找和方法调用,这就是为什么它对于非常短的数字会更慢。
只是为了提供一个小比较,即迭代字符串需要多长时间与 Python 迭代内部数组需要多长时间:
import matplotlib.pyplot as plt
from simple_benchmark import BenchmarkBuilder
%matplotlib notebook
bench = BenchmarkBuilder()
@bench.add_function()
def string_iteration(s):
# there is no "a" in the string, so this iterates over the whole string
return 'a' in s
@bench.add_function()
def python_iteration(s):
for c in s:
pass
@bench.add_arguments('string length')
def argument_provider():
for i in range(2, 20):
size = 2**i
yield size, '1'*size
plt.figure()
result = bench.run()
result.plot()
在这个基准测试中,让 Python 对字符串进行迭代比使用 for 循环对字符串进行迭代快约 200 倍。
为什么它们都对大数收敛?
这实际上是因为数字到字符串的转换将占主导地位。因此,对于非常大的数字,您实际上只是在测量将该数字转换为字符串所需的时间。
如果您将采用数字并将其转换为字符串的版本与采用转换后的数字的版本进行比较,您会看到差异(我使用another answer here 中的函数来说明这一点)。左边是数字基准,右边是采用字符串的基准 - 两个图的 y 轴也是相同的:
正如您所见,对于大数而言,采用字符串的函数的基准测试明显快于采用数字并将其转换为内部字符串的函数。这表明字符串转换是大数的“瓶颈”。为方便起见,我还包括了一个仅将字符串转换为左图的基准(这对于大数字变得重要/占主导地位)。
%matplotlib notebook
from simple_benchmark import BenchmarkBuilder
import matplotlib.pyplot as plt
import random
bench1 = BenchmarkBuilder()
@bench1.add_function()
def f1(x):
return sum(c in '02468' for c in str(x))
@bench1.add_function()
def f2(x):
return sum([c in '02468' for c in str(x)])
@bench1.add_function()
def f3(x):
return sum([True for c in str(x) if c in '02468'])
@bench1.add_function()
def f4(x):
return sum([1 for c in str(x) if c in '02468'])
@bench1.add_function()
def explicit_loop(x):
count = 0
for c in str(x):
if c in '02468':
count += 1
return count
@bench1.add_function()
def f5(x):
s = str(x)
return sum(s.count(c) for c in '02468')
bench1.add_function()(str)
@bench1.add_arguments(name='number length')
def arg_provider():
for i in range(2, 15):
size = 2 ** i
yield (2**i, int(''.join(str(random.randint(0, 9)) for _ in range(size))))
bench2 = BenchmarkBuilder()
@bench2.add_function()
def f1(x):
return sum(c in '02468' for c in x)
@bench2.add_function()
def f2(x):
return sum([c in '02468' for c in x])
@bench2.add_function()
def f3(x):
return sum([True for c in x if c in '02468'])
@bench2.add_function()
def f4(x):
return sum([1 for c in x if c in '02468'])
@bench2.add_function()
def explicit_loop(x):
count = 0
for c in x:
if c in '02468':
count += 1
return count
@bench2.add_function()
def f5(x):
return sum(x.count(c) for c in '02468')
@bench2.add_arguments(name='number length')
def arg_provider():
for i in range(2, 15):
size = 2 ** i
yield (2**i, ''.join(str(random.randint(0, 9)) for _ in range(size)))
f, (ax1, ax2) = plt.subplots(1, 2, sharey=True)
b1 = bench1.run()
b2 = bench2.run()
b1.plot(ax=ax1)
b2.plot(ax=ax2)
ax1.set_title('Number')
ax2.set_title('String')