我最近回答了一个question on a sister site,它要求提供一个功能,该功能可以对数字的所有偶数进行计数。 other answers之一包含两个功能(到目前为止是最快的):
def count_even_digits_spyr03_for(n):
count = 0
for c in str(n):
if c in "02468":
count += 1
return count
def count_even_digits_spyr03_sum(n):
return sum(c in "02468" for c in str(n))
此外,我还研究了使用列表理解和list.count
:
def count_even_digits_spyr03_list(n):
return [c in "02468" for c in str(n)].count(True)
前两个函数基本相同,除了第一个函数使用显式计数循环,而第二个函数使用内置的sum
。我本来希望第二个会更快(例如基于this answer),这是我建议如果要求进行审查的话,将第二个转变为第二个。但是,事实证明是相反的。用越来越多的数字对一些随机数进行测试(因此,任何一位数字的偶数概率约为50%),我得到以下计时:
为什么手动for
循环这么快?几乎比使用sum
快两倍。而且,由于内置sum
的速度应该比手动汇总列表大约快五倍(根据the linked answer),这意味着它实际上要快十倍!是不是因为只需要将一半的值添加到计数器中而节省了成本,因为另一半被丢弃了,足以说明这种差异?
使用if
作为过滤器,如下所示:
def count_even_digits_spyr03_sum2(n):
return sum(1 for c in str(n) if c in "02468")
仅将计时提高到与列表理解相同的水平。
将计时扩展到更大的数字并归一化为for
循环计时时,它们可能会渐渐收敛为非常大的数字(> 10k位),这可能是由于str(n)
花费的时间:>
答案 0 :(得分:29)
sum
相当快,但是sum
并不是造成速度变慢的原因。造成减速的三个主要因素:
sum
使用其整数快速路径。与列表理解相比,生成器具有两个主要优点:生成器占用的内存少得多,并且如果不需要所有元素,它们可以提前终止。在没有所有元素的情况下,它们不是旨在提供时间优势。每个元素暂停和恢复一次生成器非常昂贵。
如果我们用列表理解替换genexp:
In [66]: def f1(x):
....: return sum(c in '02468' for c in str(x))
....:
In [67]: def f2(x):
....: return sum([c in '02468' for c in str(x)])
....:
In [68]: x = int('1234567890'*50)
In [69]: %timeit f1(x)
10000 loops, best of 5: 52.2 µs per loop
In [70]: %timeit f2(x)
10000 loops, best of 5: 40.5 µs per loop
我们看到立即加速,但以浪费列表上的大量内存为代价。
如果您查看genexp版本:
def count_even_digits_spyr03_sum(n):
return sum(c in "02468" for c in str(n))
您会看到它没有if
。它只是将布尔值扔到sum
中。相反,您的循环:
def count_even_digits_spyr03_for(n):
count = 0
for c in str(n):
if c in "02468":
count += 1
return count
仅在数字为偶数时添加任何内容。
如果我们将前面定义的f2
更改为也包含if
,我们会看到另一个加速:
In [71]: def f3(x):
....: return sum([True for c in str(x) if c in '02468'])
....:
In [72]: %timeit f3(x)
10000 loops, best of 5: 34.9 µs per loop
f1
与原始代码相同,花费了52.2 µs,而f2
仅对列表理解进行了更改,花费了40.5 µs。
使用True
代替1
中的f3
可能看起来很尴尬。那是因为将其更改为1
会激活一个最终的加速。 sum
的整数为fast path,但是快速路径仅为类型恰好为int
的对象激活。 bool
不算在内。这是检查项目是否为int
类型的行:
if (PyLong_CheckExact(item)) {
做出最后更改后,将True
更改为1
:
In [73]: def f4(x):
....: return sum([1 for c in str(x) if c in '02468'])
....:
In [74]: %timeit f4(x)
10000 loops, best of 5: 33.3 µs per loop
我们看到了最后一个小加速。
那么,毕竟,我们打败了显式循环吗?
In [75]: def explicit_loop(x):
....: count = 0
....: for c in str(x):
....: if c in '02468':
....: count += 1
....: return count
....:
In [76]: %timeit explicit_loop(x)
10000 loops, best of 5: 32.7 µs per loop
不。我们已经基本达到收支平衡,但是我们没有击败它。剩下的最大问题是列表。构建它很昂贵,并且sum
必须通过列表迭代器来检索元素,而元素有其自身的成本(尽管我认为这很便宜)。不幸的是,只要我们使用测试数字和呼叫sum
方法,我们就没有摆脱该列表的好方法。显式循环获胜。
反正我们可以走得更远吗?好吧,到目前为止,我们一直在尝试使sum
更接近显式循环,但是如果我们坚持使用这个愚蠢的列表,我们可以脱离显式循环,而只需调用len
之sum
:
def f5(x):
return len([1 for c in str(x) if c in '02468'])
单独测试数字并不是我们尝试打破循环的唯一方法。与显式循环相比,我们还可以尝试str.count
。 str.count
直接在C语言中循环访问字符串缓冲区,从而避免了很多包装对象和间接调用。我们需要调用它5次,使字符串经过5次传递,但仍然有回报:
def f6(x):
s = str(x)
return sum(s.count(c) for c in '02468')
不幸的是,这就是我用于计时的站点将我困在“ tarpit”中以占用过多资源的原因,因此我不得不切换站点。以下时间不能与上述时间直接比较:
>>> import timeit
>>> def f(x):
... return sum([1 for c in str(x) if c in '02468'])
...
>>> def g(x):
... return len([1 for c in str(x) if c in '02468'])
...
>>> def h(x):
... s = str(x)
... return sum(s.count(c) for c in '02468')
...
>>> x = int('1234567890'*50)
>>> timeit.timeit(lambda: f(x), number=10000)
0.331528635986615
>>> timeit.timeit(lambda: g(x), number=10000)
0.30292080697836354
>>> timeit.timeit(lambda: h(x), number=10000)
0.15950968803372234
>>> def explicit_loop(x):
... count = 0
... for c in str(x):
... if c in '02468':
... count += 1
... return count
...
>>> timeit.timeit(lambda: explicit_loop(x), number=10000)
0.3305045129964128
答案 1 :(得分:9)
如果使用dis.dis()
,我们可以看到函数的实际行为。
count_even_digits_spyr03_for()
:
7 0 LOAD_CONST 1 (0)
3 STORE_FAST 0 (count)
8 6 SETUP_LOOP 42 (to 51)
9 LOAD_GLOBAL 0 (str)
12 LOAD_GLOBAL 1 (n)
15 CALL_FUNCTION 1 (1 positional, 0 keyword pair)
18 GET_ITER
>> 19 FOR_ITER 28 (to 50)
22 STORE_FAST 1 (c)
9 25 LOAD_FAST 1 (c)
28 LOAD_CONST 2 ('02468')
31 COMPARE_OP 6 (in)
34 POP_JUMP_IF_FALSE 19
10 37 LOAD_FAST 0 (count)
40 LOAD_CONST 3 (1)
43 INPLACE_ADD
44 STORE_FAST 0 (count)
47 JUMP_ABSOLUTE 19
>> 50 POP_BLOCK
11 >> 51 LOAD_FAST 0 (count)
54 RETURN_VALUE
我们可以看到只有一个函数调用,即一开始是str()
:
9 LOAD_GLOBAL 0 (str)
...
15 CALL_FUNCTION 1 (1 positional, 0 keyword pair)
其余部分是高度优化的代码,使用跳转,存储和就地添加。
count_even_digits_spyr03_sum()
会发生什么:
14 0 LOAD_GLOBAL 0 (sum)
3 LOAD_CONST 1 (<code object <genexpr> at 0x10dcc8c90, file "test.py", line 14>)
6 LOAD_CONST 2 ('count2.<locals>.<genexpr>')
9 MAKE_FUNCTION 0
12 LOAD_GLOBAL 1 (str)
15 LOAD_GLOBAL 2 (n)
18 CALL_FUNCTION 1 (1 positional, 0 keyword pair)
21 GET_ITER
22 CALL_FUNCTION 1 (1 positional, 0 keyword pair)
25 CALL_FUNCTION 1 (1 positional, 0 keyword pair)
28 RETURN_VALUE
虽然我不能完全解释这些差异,但我们可以清楚地看到有更多的函数调用(可能是sum()
和in
(?)),它们使代码的运行比执行慢得多。机器指令直接显示。
答案 2 :(得分:9)
@MarkusMeskanen的答案正确无误–函数调用很慢,genexprs和listcomps基本上都是函数调用。
无论如何,要务实:
使用str.count(c)
更快,而this related answer of mine about strpbrk()
in Python可以使事情变得更快。
def count_even_digits_spyr03_count(n):
s = str(n)
return sum(s.count(c) for c in "02468")
def count_even_digits_spyr03_count_unrolled(n):
s = str(n)
return s.count("0") + s.count("2") + s.count("4") + s.count("6") + s.count("8")
结果:
string length: 502
count_even_digits_spyr03_list 0.04157966522
count_even_digits_spyr03_sum 0.05678154459
count_even_digits_spyr03_for 0.036128606150000006
count_even_digits_spyr03_count 0.010441866129999991
count_even_digits_spyr03_count_unrolled 0.009662931009999999
答案 3 :(得分:4)
实际上有一些差异会导致观察到的性能差异。我的目标是对这些差异进行高层次的概述,但尽量不要过多介绍低层次的细节或可能的改进。对于基准测试,我使用自己的软件包simple_benchmark
。
生成器和生成器表达式是语法糖,可用来代替编写迭代器类。
编写类似以下的生成器时:
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()
要了解与循环(显式或隐式)相比使用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')
答案 4 :(得分:0)
您的所有函数都包含对TableLayout
(一次调用)和str(n)
(对于n中的每个c)的相等数量的调用。从那时起,我想简化一下:
c in "02468"
import timeit
num = ''.join(str(i % 10) for i in range(1, 10000001))
def count_simple_sum():
return sum(1 for c in num)
def count_simple_for():
count = 0
for c in num:
count += 1
return count
print('For Loop Sum:', timeit.timeit(count_simple_for, number=10))
print('Built-in Sum:', timeit.timeit(count_simple_sum, number=10))
仍然较慢:
sum
这两个函数之间的主要区别在于,在For Loop Sum: 2.8987821330083534
Built-in Sum: 3.245505138998851
中,您仅使用纯for循环count_simple_for
迭代抛出num
,而在for c in num
中,您正在创建{ {1}}个对象(来自@Markus Meskanen answer with dis.dis
):
count_simple_sum
generator
遍历此生成器对象以求和所生成的元素,并且此生成器遍历num个元素以在每个元素上生成 3 LOAD_CONST 1 (<code object <genexpr> at 0x10dcc8c90, file "test.py", line 14>)
6 LOAD_CONST 2 ('count2.<locals>.<genexpr>')
。再进行一次迭代是很昂贵的,因为它需要在每个元素上调用sum
并将这些调用放在1
块中,这也会增加一些开销。