Вступление
Ресурсов никогда не бывает достаточно для удовлетворения растущих потребностей в большинстве отраслей, особенно в технологиях, которые все глубже проникают в нашу жизнь. Технологии делают жизнь проще и удобнее, и со временем они могут развиваться и становиться лучше.
Эта возросшая зависимость от технологий произошла за счет доступных вычислительных ресурсов. В результате разрабатываются более мощные компьютеры, и оптимизация кода как никогда важна.
Требования к производительности приложений растут быстрее, чем может справиться наше оборудование. Чтобы бороться с этим, люди придумали множество стратегий для более эффективного использования ресурсов - контейнеризация , реактивные (асинхронные) приложения и т. Д.
Тем не менее, первый шаг, который мы должны сделать, и, безусловно, самый простой, который нужно принять во внимание, - это оптимизация кода . Нам нужно написать код, который работает лучше и использует меньше вычислительных ресурсов.
В этой статье мы оптимизируем общие шаблоны и процедуры в программировании на Python, чтобы повысить производительность и улучшить использование доступных вычислительных ресурсов.
Проблема с производительностью
По мере масштабирования программных решений производительность
становится все более важной, а проблемы - более масштабными и заметными.
Когда мы пишем код на нашем localhost
, легко упустить некоторые
проблемы с производительностью, поскольку использование не интенсивное.
Когда одно и то же программное обеспечение развертывается для тысяч и
сотен тысяч одновременно работающих конечных пользователей, проблемы
становятся более сложными.
Медлительность - одна из основных проблем, возникающих при масштабировании программного обеспечения. Для этого характерно увеличенное время отклика. Например, веб-серверу может потребоваться больше времени для обслуживания веб-страниц или отправки ответов клиентам, когда запросов становится слишком много. Никому не нравится медленная система, особенно потому, что технология предназначена для ускорения определенных операций, а удобство использования ухудшится, если система будет медленной.
Когда программное обеспечение не оптимизировано для надлежащего использования доступных ресурсов, ему в конечном итоге потребуется больше ресурсов для обеспечения бесперебойной работы. Например, если управление памятью не выполняется должным образом, программе потребуется больше памяти, что приведет к увеличению затрат на обновление или частым сбоям.
Несогласованность и ошибочный вывод - еще один результат плохо оптимизированных программ. Эти моменты подчеркивают необходимость оптимизации программ.
Зачем и когда оптимизировать
При создании для крупномасштабного использования оптимизация является важным аспектом программного обеспечения. Оптимизированное программное обеспечение способно обрабатывать большое количество одновременных пользователей или запросов, легко поддерживая уровень производительности с точки зрения скорости.
Это приводит к общему удовлетворению потребностей клиентов, поскольку это не влияет на использование. Это также приводит к меньшему количеству головной боли, когда приложение вылетает посреди ночи, и ваш сердитый менеджер звонит вам, чтобы исправить это немедленно.
Вычислительные ресурсы дороги, и оптимизация может пригодиться для снижения эксплуатационных расходов с точки зрения хранения, памяти или вычислительной мощности.
Но когда мы оптимизируем?
Важно отметить, что оптимизация может негативно повлиять на читабельность и ремонтопригодность кодовой базы, сделав ее более сложной. Следовательно, важно учитывать результат оптимизации с учетом технической задолженности, которую она вызовет.
Если мы создаем большие системы, которые ожидают интенсивного взаимодействия со стороны конечных пользователей, то нам нужно, чтобы наша система работала в наилучшем состоянии, а это требует оптимизации. Кроме того, если у нас ограниченные ресурсы с точки зрения вычислительной мощности или памяти, оптимизация будет иметь большое значение для обеспечения того, чтобы мы могли обойтись доступными нам ресурсами.
Профилирование
Прежде чем мы сможем оптимизировать наш код, он должен работать. Таким образом, мы сможем узнать, как он работает и использует ресурсы. И это подводит нас к первому правилу оптимизации - не надо .
Как сказал Дональд Кнут - математик, компьютерный ученый и профессор Стэнфордского университета:
«Преждевременная оптимизация - корень всех зол».
Решение должно работать, чтобы его можно было оптимизировать.
Профилирование влечет за собой тщательное изучение нашего кода и анализ его производительности, чтобы определить, как наш код работает в различных ситуациях и областях, которые можно улучшить, если это необходимо. Это позволит нам определить количество времени, которое занимает наша программа, или объем памяти, который она использует в своих операциях. Эта информация жизненно важна в процессе оптимизации, поскольку помогает нам решить, оптимизировать наш код или нет.
Профилирование может быть сложной задачей и потребовать много времени, и если оно выполняется вручную, некоторые проблемы, влияющие на производительность, могут быть упущены. С этой целью различные инструменты, которые могут помочь профилировать код быстрее и эффективнее, включают:
- PyCallGraph - который создает визуализации графа вызовов, которые представляют отношения вызовов между подпрограммами для кода Python.
- cProfile - который описывает, как часто и как долго выполняются различные части кода Python.
- gProf2dot - это библиотека, которая визуализирует вывод профилировщиков в виде точечного графика.
Профилирование поможет нам определить области нашего кода для оптимизации. Давайте обсудим, как выбор правильной структуры данных или потока управления может помочь нашему коду Python работать лучше.
Выбор структур данных и потока управления
Выбор структуры данных в нашем коде или реализованном алгоритме может повлиять на производительность нашего кода Python. Если мы сделаем правильный выбор в отношении наших структур данных, наш код будет работать хорошо.
Профилирование может оказаться большим подспорьем в определении наилучшей структуры данных для использования в различных точках кода Python. Много ли вставок делаем? Мы часто удаляем? Мы постоянно ищем товары? Такие вопросы могут помочь нам выбрать правильную структуру данных в соответствии с потребностями и, следовательно, привести к оптимизации кода Python.
На время и использование памяти сильно повлияет наш выбор структуры данных. Также важно отметить, что некоторые структуры данных по-разному реализованы на разных языках программирования.
Для циклов и списков
Циклы являются обычным явлением при разработке на Python, и довольно скоро вы столкнетесь с пониманием списков, которые являются кратким способом создания новых списков, которые также поддерживают условия.
Например, если мы хотим получить список квадратов всех четных чисел в
определенном диапазоне, используя for loop
:
new_list = []
for n in range(0, 10):
if n % 2 == 0:
new_list.append(n**2)
Версия цикла со List Comprehension
new_list = [ n**2 for n in range(0,10) if n%2 == 0]
Список стал короче и лаконичнее, но это не единственный трюк в рукаве. Они также значительно быстрее по времени выполнения, чем циклы. Мы будем использовать модуль Timeit, который позволяет синхронизировать небольшие фрагменты кода Python.
Давайте сопоставим понимание списка с эквивалентом for
и посмотрим,
сколько времени требуется каждому для достижения одного и того же
результата:
import timeit
def for_square(n):
new_list = []
for i in range(0, n):
if i % 2 == 0:
new_list.append(n**2)
return new_list
def list_comp_square(n):
return [i**2 for i in range(0, n) if i % 2 == 0]
print("Time taken by For Loop: {}".format(timeit.timeit('for_square(10)', 'from __main__ import for_square')))
print("Time taken by List Comprehension: {}".format(timeit.timeit('list_comp_square(10)', 'from __main__ import list_comp_square')))
После запуска скрипта 5 раз с использованием Python 2:
$ python for-vs-lc.py
Time taken by For Loop: 2.56907987595
Time taken by List Comprehension: 2.01556396484
$
$ python for-vs-lc.py
Time taken by For Loop: 2.37083697319
Time taken by List Comprehension: 1.94110512733
$
$ python for-vs-lc.py
Time taken by For Loop: 2.52163410187
Time taken by List Comprehension: 1.96427607536
$
$ python for-vs-lc.py
Time taken by For Loop: 2.44279003143
Time taken by List Comprehension: 2.16282701492
$
$ python for-vs-lc.py
Time taken by For Loop: 2.63641500473
Time taken by List Comprehension: 1.90950393677
Хотя разница непостоянна, понимание списка занимает меньше времени, чем
цикл for
В мелкомасштабном коде это может не иметь большого значения,
но при крупномасштабном выполнении это может быть вся разница,
необходимая для экономии времени.
Если мы увеличим диапазон квадратов с 10 до 100, разница станет более очевидной:
$ python for-vs-lc.py
Time taken by For Loop: 16.0991549492
Time taken by List Comprehension: 13.9700510502
$
$ python for-vs-lc.py
Time taken by For Loop: 16.6425571442
Time taken by List Comprehension: 13.4352738857
$
$ python for-vs-lc.py
Time taken by For Loop: 16.2476081848
Time taken by List Comprehension: 13.2488780022
$
$ python for-vs-lc.py
Time taken by For Loop: 15.9152050018
Time taken by List Comprehension: 13.3579590321
cProfile - это профилировщик, который поставляется с Python, и если мы используем его для профилирования нашего кода:
{.ezlazyload}
При дальнейшем рассмотрении мы все еще можем видеть, что инструмент cProfile сообщает, что наше понимание списка требует меньше времени на выполнение, чем наша реализация цикла For Loop , как мы установили ранее. cProfile отображает все вызванные функции, количество их вызовов и время, затраченное каждой из них.
Если наше намерение состоит в том, чтобы сократить время, затрачиваемое на выполнение нашего кода, то лучше использовать List Component, чем цикл For Loop. Эффект от такого решения по оптимизации нашего кода будет намного яснее в большем масштабе и покажет, насколько важной, но в то же время простой может быть оптимизация кода.
Но что, если нас беспокоит использование памяти? Понимание списка
потребует больше памяти для удаления элементов в списке, чем обычный
цикл. Понимание списка всегда создает новый список в памяти по
завершении, поэтому для удаления элементов из списка будет создан новый
список. В то время как для обычного цикла for мы можем использовать
list.remove()
или list.pop()
для изменения исходного списка вместо
создания нового в памяти.
Опять же, в мелкомасштабных сценариях это может не иметь большого значения, но оптимизация хороша в большем масштабе, и в этой ситуации такая экономия памяти будет хорошей и позволит нам использовать дополнительную память, сохраненную для других операций.
Связанные списки
Еще одна структура данных, которая может пригодиться для экономии памяти, - это связанный список . Он отличается от обычного массива тем, что каждый элемент или узел имеет ссылку или указатель на следующий узел в списке и не требует непрерывного выделения памяти.
Массив требует, чтобы память, необходимая для его хранения и его элементов, была выделена заранее, и это может быть довольно дорого или расточительно, если размер массива заранее неизвестен.
Связанный список позволит вам распределять память по мере необходимости. Это возможно, потому что узлы в связанном списке могут храниться в разных местах памяти, но объединяются в связанный список с помощью указателей. Это делает связанные списки более гибкими по сравнению с массивами.
Предостережение со связанным списком заключается в том, что время поиска медленнее, чем у массива, из-за размещения элементов в памяти. Правильное профилирование поможет вам определить, нужна ли вам лучшая память или управление временем, чтобы решить, использовать ли связанный список или массив в качестве структуры данных при оптимизации кода.
Диапазон против XRange
При работе с циклами в Python иногда нам нужно сгенерировать список
целых чисел, чтобы помочь нам в выполнении циклов for. Для этого
используются функции range
и xrange
.
Их функциональность такая же, но они отличаются тем, что range
возвращает объект list
xrange
возвращает объект xrange
Что это значит? Объект xrange является
генератором xrange
что это не окончательный
список. Это дает нам возможность генерировать значения в ожидаемом
окончательном списке по мере необходимости во время выполнения с помощью
метода, известного как «уступка».
Тот факт, что xrange
не возвращает окончательный список, делает ее
более эффективным с точки зрения памяти выбором для создания огромных
списков целых чисел для целей цикла.
Если нам нужно сгенерировать большое количество целых чисел для
использования, нам xrange
поскольку он использует меньше памяти. Если
вместо этого мы используем range
, необходимо будет создать весь
список целых чисел, и это потребует большого объема памяти.
Давайте исследуем разницу в потреблении памяти между двумя функциями:
$ python
Python 2.7.10 (default, Oct 23 2015, 19:19:21)
[GCC 4.2.1 Compatible Apple LLVM 7.0.0 (clang-700.0.59.5)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> import sys
>>>
>>> r = range(1000000)
>>> x = xrange(1000000)
>>>
>>> print(sys.getsizeof(r))
8000072
>>>
>>> print(sys.getsizeof(x))
40
>>>
>>> print(type(r))
<type 'list'>
>>> print(type(x))
<type 'xrange'>
Мы создаем диапазон из 1000000 целых чисел, используя range
и xrange
. Тип объекта, созданного range
- это List
который потребляет
8000072 bytes
памяти, в то время xrange
объект xrange потребляет
только 40 bytes
памяти.
Функция xrange
экономит много памяти, но как насчет времени поиска
элемента? Давайте рассчитаем время поиска целого числа в сгенерированном
списке целых чисел с помощью Timeit:
import timeit
r = range(1000000)
x = xrange(1000000)
def lookup_range():
return r[999999]
def lookup_xrange():
return x[999999]
print("Look up time in Range: {}".format(timeit.timeit('lookup_range()', 'from __main__ import lookup_range')))
print("Look up time in Xrange: {}".format(timeit.timeit('lookup_xrange()', 'from __main__ import lookup_xrange')))
Результат:
$ python range-vs-xrange.py
Look up time in Range: 0.0959858894348
Look up time in Xrange: 0.140854120255
$
$ python range-vs-xrange.py
Look up time in Range: 0.111716985703
Look up time in Xrange: 0.130584001541
$
$ python range-vs-xrange.py
Look up time in Range: 0.110965013504
Look up time in Xrange: 0.133008003235
$
$ python range-vs-xrange.py
Look up time in Range: 0.102388143539
Look up time in Xrange: 0.133061170578
xrange
может потреблять меньше памяти, но для поиска в нем элемента
требуется больше времени. Учитывая ситуацию и доступные ресурсы, мы
можем выбрать range
или xrange
зависимости от того, к чему мы
стремимся. Это подтверждает важность профилирования для оптимизации
нашего кода Python.
Примечание. xrange
устарела в Python 3, и range
теперь может
выполнять те же функции. Генераторы по-прежнему доступны в Python 3 и
могут помочь нам сэкономить память другими способами, такими как
генераторы понимания или
выражений .
Наборы
При работе со списками в Python нужно помнить, что они допускают повторяющиеся записи. Что, если имеет значение, содержат ли наши данные дубликаты?
Вот тут-то и пригодятся наборы Python . Они похожи на списки, но не позволяют хранить в них дубликаты. Наборы также используются для эффективного удаления дубликатов из списков и работают быстрее, чем создание нового списка и заполнение его дубликатами.
В этой операции вы можете думать о них как о воронке или фильтре, который удерживает дубликаты и пропускает только уникальные значения.
Сравним две операции:
import timeit
# here we create a new list and add the elements one by one
# while checking for duplicates
def manual_remove_duplicates(list_of_duplicates):
new_list = []
[new_list.append(n) for n in list_of_duplicates if n not in new_list]
return new_list
# using a set is as simple as
def set_remove_duplicates(list_of_duplicates):
return list(set(list_of_duplicates))
list_of_duplicates = [10, 54, 76, 10, 54, 100, 1991, 6782, 1991, 1991, 64, 10]
print("Manually removing duplicates takes {}s".format(timeit.timeit('manual_remove_duplicates(list_of_duplicates)', 'from __main__ import manual_remove_duplicates, list_of_duplicates')))
print("Using Set to remove duplicates takes {}s".format(timeit.timeit('set_remove_duplicates(list_of_duplicates)', 'from __main__ import set_remove_duplicates, list_of_duplicates')))
После пяти запусков скрипта:
$ python sets-vs-lists.py
Manually removing duplicates takes 2.64614701271s
Using Set to remove duplicates takes 2.23225092888s
$
$ python sets-vs-lists.py
Manually removing duplicates takes 2.65356898308s
Using Set to remove duplicates takes 1.1165189743s
$
$ python sets-vs-lists.py
Manually removing duplicates takes 2.53129696846s
Using Set to remove duplicates takes 1.15646100044s
$
$ python sets-vs-lists.py
Manually removing duplicates takes 2.57102680206s
Using Set to remove duplicates takes 1.13189387321s
$
$ python sets-vs-lists.py
Manually removing duplicates takes 2.48338890076s
Using Set to remove duplicates takes 1.20611810684s
Использование набора для удаления дубликатов намного быстрее, чем создание списка вручную и добавление элементов при проверке присутствия.
Это может быть полезно при фильтрации записей для розыгрыша призов, когда мы должны отфильтровывать повторяющиеся записи. Если для фильтрации 120 записей требуется 2 секунды, представьте себе фильтрацию 10 000 записей. В таком масштабе значительно возросшая производительность, предоставляемая с помощью Sets.
Это может происходить нечасто, но при необходимости может иметь огромное значение. Правильное профилирование может помочь нам определить такие ситуации и существенно повлиять на производительность нашего кода.
Конкатенация строк
По умолчанию строки в Python неизменяемы, и впоследствии объединение строк может быть довольно медленным. Есть несколько способов объединения строк, применимых к различным ситуациям.
Мы можем использовать +
(плюс) для соединения строк. Это идеально
подходит для нескольких объектов String и не масштабируется. Если вы
используете +
для объединения нескольких строк, каждое объединение
создаст новый объект, поскольку строки неизменяемы. Это приведет к
созданию множества новых объектов String в памяти, следовательно, к
неправильному использованию памяти.
Мы также можем использовать оператор конкатенации +=
для объединения
строк, но это работает только для двух строк одновременно, в отличие от
+
который может объединять более двух строк.
Если у нас есть итератор, такой как List, который имеет несколько строк,
идеальный способ .join()
.
Давайте создадим список из тысячи слов и сравним, как сравниваются
.join()
и оператор +=
import timeit
# create a list of 1000 words
list_of_words = ["foo "] * 1000
def using_join(list_of_words):
return "".join(list_of_words)
def using_concat_operator(list_of_words):
final_string = ""
for i in list_of_words:
final_string += i
return final_string
print("Using join() takes {} s".format(timeit.timeit('using_join(list_of_words)', 'from __main__ import using_join, list_of_words')))
print("Using += takes {} s".format(timeit.timeit('using_concat_operator(list_of_words)', 'from __main__ import using_concat_operator, list_of_words')))
После двух попыток:
$ python join-vs-concat.py
Using join() takes 14.0949640274 s
Using += takes 79.5631570816 s
$
$ python join-vs-concat.py
Using join() takes 13.3542580605 s
Using += takes 76.3233859539 s
Очевидно, что метод .join()
не только более аккуратный и читаемый, но
и значительно быстрее, чем оператор конкатенации, при соединении строк в
итераторе.
Если вы выполняете много операций конкатенации строк, замечательно пользоваться преимуществами подхода, который почти в 7 раз быстрее.
Заключение
Мы установили, что оптимизация кода имеет решающее значение для Python, а также увидели разницу, когда она масштабируется. С помощью модуля Timeit и профилировщика cProfile мы смогли определить, какая реализация требует меньше времени для выполнения, и подкрепили ее цифрами. Структуры данных и структуры потока управления, которые мы используем, могут сильно повлиять на производительность нашего кода, и нам следует быть более осторожными.
Профилирование также является важным шагом в оптимизации кода, поскольку оно направляет процесс оптимизации и делает его более точным. Мы должны быть уверены, что наш код работает и верен, прежде чем оптимизировать его, чтобы избежать преждевременной оптимизации, которая может оказаться более дорогостоящей в обслуживании или затруднить понимание кода.