Есть несколько различий, которые на самом деле способствуют наблюдаемым различиям в производительности.Я стремлюсь дать общий обзор этих различий, но стараюсь не вдаваться в подробности низкого уровня или возможные улучшения.Для тестов я использую свой собственный пакет 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 выполняет итерацию по строке в ~ 200 раз быстрее, чем итерацию по строке с циклом for.
Почему все они сходятся для больших чисел?
Это на самом деле потому, что преобразование числа в строку будет доминирующим там.Таким образом, для действительно огромных чисел вы просто измеряете, сколько времени нужно, чтобы преобразовать это число в строку.
Вы увидите разницу, если вы сравните версии, которые принимают число и преобразовали его в строку.с тем, который принимает преобразованное число (я использую функции из другой ответ здесь , чтобы проиллюстрировать это).Слева - эталон числа, а справа - эталон, который принимает строки - также ось 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')