Функции vs генераторы: производительность, особенности, размер
Что такое генераторы
При стандартном подходе используются функции, которые возвращают списки. Допустим, вот такая функция, которая возвращает все степени двойки до 1000:
def degrees_two():
result = []
i = 0
while 2**i <= 1000:
result.append(2**i)
i += 1
return resultТакие функции просты в использовании, но неэффективны при работе с большими данными. Допустим, если задать возвращение степеней двойки до миллиарда, то программа просто "замрёт", пока всё это не посчитает, если, конечно, уместит всё это в памяти. Но генераторы не пытаются всё сохранить. Они только запоминают, какое значение сейчас и как двигаться дальше. При этом не придётся ждать, пока вернутся все значения. Они возвращаются поочерёдно.
Минусы генераторов
Но у генераторов также есть и минусы:
Во-первых, генератор можно использовать ТОЛЬКО ОДИН РАЗ. После этого при попытке его использовать просто ничего не произойдёт. Это создаёт минус там, где надо повторять действие много раз.
Во-вторых, он не просто возвращает готовые значения. Чтобы их получить, надо пройтись по генератору циклом.
Метрики
Всего я буду измерять по двум метрикам: время выполнения и место в памяти. Измерять буду генератор, уже проитерированный в цикле генератор и обычная функция. Для начала импортируем библиотеки замера времени и размера: sys и time. Использоваться будут функции sys.getsizeof() и time.time(). Напишем функцию для поиска делителей:
def primes(n):
result = list()
for i in range(1, n//2+1):
if n%i == 0:
result.append(i)
return resultИ функцию-генератор, которая возвращает числа при помощи ключевого слова yield:
def gen_primes(n):
for i in range(1, n//2+1):
if n%i == 0:
yield i
Далее создадим собственно объекты замера и вывод:
start = time.time()
example = primes()
end = time.time()
time1 = int(round((end-start)*1000000))
start_gen = time.time()
example_gen = [x for x in gen_primes()]
end_gen = time.time()
time2 = int(round((end_gen-start_gen)*1000000))print(f"Метрика 1: Время \n\tОбычная функция: {time1} микросекунд.\n\tГенератор: {time2} микросекунд.")
size1 = sys.getsizeof(example)
size2 = sys.getsizeof(example_gen)
print(f"Метрика 2: Размер \n\tОбычная функция: {size1} байт.\n\tГенератор: {size2} байт.")В тестировании будет 3 раунда:
Раунд 1 - 1000
Раунд2 - 100тыс.
Раунд 3 - 10млн
Запускаем первый раунд и видим вывод:
Метрика 1: Время Обычная функция: 105 микросекунд. Генератор: 80 микросекунд.
Метрика 2: Размер
Обычная функция: 184 байт.
Генератор: 184 байт.
Как видим, проитерированный генератор выполнился быстрее, но память расходована одинаково.
Запускаем второй раунд, со значением 100 тысяч и встречаем вывод:
Метрика 1: Время Обычная функция: 16376 микросекунд. Генератор: 14217 микросекунд.
Метрика 2: Размер
Обычная функция: 376 байт.
Генератор: 376 байт.
Разница во времени и осталась ~⅛. Запустим 3 раунд, значение - 10 миллионов:
Метрика 1: Время Обычная функция: 1148705 микросекунд. Генератор: 1161036 микросекунд.
Метрика 2: Размер
Обычная функция: 568 байт.
Генератор: 568 байт.
В обоих случаях время примерно секунда с лимним, правда, тут генератор начал отставать. При повторном запуске, наоборот, генератор был быстрее:
Метрика 1: Время Обычная функция: 1263085 микросекунд. Генератор: 1130301 микросекунд.
Метрика 2: Размер
Обычная функция: 568 байт.
Генератор: 568 байт.
Причем как раз на одну восьмую. Поэтому, для чистоты эксперимента я дополнительно попробовал эти функции в 1 миллиарде чисел. Итак, барабанная дробь, вот:
Метрика 1: Время
Обычная функция: 113 820 924 микросекунд.
Генератор: 115 045 928 микросекунд.
Метрика 2: Размер
Обычная функция: 920 байт.
Генератор: 920 байт.
В итоге генератор отстал. Это длилось почти две минуты. Перезапускать я не стал, если хотите, попробуйте сами.
Теперь займусь генератором ещё не проитерированным:
Метрика 1: Время Обычная функция: 196 микросекунд. Генератор: 5 микросекунд.
Метрика 2: Размер Обычная функция: 184 байт. Генератор: 216 байт.
Как ни странно, в таком виде генератор занимает больше, чем функция. А по времени он "успел" на 5 микросекунд. То есть большую часть времени занимает именно итерирование циклом. Но при этом в последующих раундах этот размер и время остаются стабильными, когда у обычных функций растёт.
Итог
Итак, генераторы выигрывают по времени от функций, но незначительно, а на больших "высотах" и то не всегда. Памятью уже проитерированные одинаковы с функциями. Но просто генератор создаётся очень быстро и имеет фиксированный объём в памяти, будь параметром хоть 10, хоть миллиард.