Профилирование программы на Python — это динамический анализ, который измеряет время выполнения программы — сколько времени требуется коду для выполнения каждой функции программы. Поскольку функции и вызовы требуют слишком много ресурсов, их необходимо оптимизировать. А оптимизация кода неизбежно ведет к оптимизации затрат, поскольку использует меньше ресурсов ЦП, а значит, меньше платит за облачную инфраструктуру.
Разработчики часто используют разные подходы для локальной оптимизации. Например, они определяют, какая функция быстрее выполняет код. Однако, если мы попытаемся настроить программу вслепую без расследования, это может вызвать проблемы и привести к замедлению выполнения программы.
Как выполняется профилирование?
Необходимо решить, нужно ли использовать методологию профилирования на макро- или микроуровне. При макропрофилировании мы профилируем всю программу и производим статистические данные во время ее выполнения. При микропрофилировании профилируется только конкретный компонент программы, как вызов функции или вычисление.
4 совета как ЛУЧШЕ писать циклы For на Python
В зависимости от того, хотите ли вы выполнять макро- или микропрофилирование, в Python доступно несколько модулей и встроенных функций. Для микропрофилирования мы можем использовать line_profiler, а для макросов мы можем использовать cProfile или временной пакет, с помощью которого мы можем определить общее время, необходимое программе для выполнения.
Иногда, когда мы используем пакет времени, полученное время может быть неправильным из-за задержки, вызванной задержкой в сети или загруженным процессором. Итак, функции timeit выполняют код несколько раз, а затем показывают среднее время выполнения.
Непрерывное профилирование и время выполнения
Самое важное, что нужно учитывать, — это время, необходимое для выполнения программы. Чем больше времени займет программа, тем медленнее будет работать приложение, использующее эту программу. Кроме того, пока он реализуется, он занимает слишком много ресурсов ЦП. Чем больше ресурсов вы используете для запуска приложения в облаке, тем больше вам придется платить.
Давайте рассмотрим несколько распространенных причин, по которым время выполнения увеличивается.
Устранение утечек и использования памяти
Процесс чтения и записи данных называется управлением памятью. По умолчанию это реализовано на Python.
Однако есть некоторые сценарии, в которых могут возникнуть утечки памяти.
- Когда не освобождаются остаточные крупные объекты.
- Когда в коде есть ссылочные циклы.
Предположим, что был создан сервер для кэширования, но размер кеша не был определен. Это будет означать, что значительное количество запросов может увеличить размер кеша до точки утечки памяти.
Python управляет своей собственной кучей, отдельной от системной кучи. Память выделяется в интерпретаторе Python — мы используем для этого разные методы в зависимости от типа объекта, который необходимо создать. Пулы и арены сохраняются в куче Python, поскольку Python обрабатывает свой собственный блок. Освобождение любого типа блока памяти просто отмечает его как доступный для будущего использования в интерпретаторе.
Самый БЫСТРЫЙ стандартный цикл Python − Интеграция с языком Си
Чтобы найти утечку памяти, мы используем профилировщик памяти, который построчно отображает использование памяти кодом. Таким образом, мы можем выяснить, какие выражения потребляют значительный объем оперативной памяти. После этого мы можем оптимизировать эти операторы, чтобы они использовали меньше оперативной памяти.
Правильное использование ресурсов ЦП
Важно оптимизировать использование ресурсов ЦП. Если вы используете облачные сервисы, это гораздо важнее, потому что чем больше ресурсов вы используете, тем больше вам придется платить. Теперь, если у нас нет статистических данных о программе, мы не можем слепо начинать процесс оптимизации.
В этом сценарии лучше всего использовать cProfile, который отображает полные статистические данные программы и перечисляет используемые функции — это дает обзор того, какие функции или операторы нуждаются в оптимизации. Правильная оптимизация кода потребует меньше ресурсов ЦП, что приведет к экономии или оптимизации для компании.
По этим и многим другим причинам разработчики на этапе производства используют непрерывное профилирование для оценки системы или программного обеспечения, чтобы увидеть, что работает медленно или использует значительный объем ресурсов. Например, непрерывное профилирование помогает идентифицировать любые инструкции или функции программы, для выполнения которых может потребоваться много оперативной памяти или времени.
Проведение непрерывного профилирования
Проблема со встроенными модулями Python заключается в том, что нельзя отслеживать статистику кода, пока он выполняется на производственном сервере. Даже если вам удастся включить производственное профилирование, оно потребляет значительный объем ресурсов ЦП, что становится слишком дорогостоящим для компании.
Поскольку непрерывное профилирование имеет важное значение, вы можете использовать gProfiler, который является модулем plug-and-play. Просто скачайте клиент и установите его на свой сервер. Он будет работать в фоновом режиме, не используя слишком много ресурсов, и будет следить за всем. Он также обеспечивает бесшовное производственное профилирование и множество статистических данных, которые профилировщик Python не может. Он потребляет меньше ресурсов ЦП, что позволяет команде сэкономить . Вы также можете установить его в Docker, используя его как Daemon Set, или установить через командную строку.
Давайте посмотрим, как его настроить и использовать. Это легко.
Во-первых, вам необходимо создать учетную запись, поскольку для мониторинга ваших сервисов требуется ключ API. Затем вы просто загружаете и устанавливаете его с помощью wget .
После создания учетной записи выполните эти команды на своем терминале:
wget https://github.com/Granulate/gprofiler/releases/latest/download/gprofiler sudo chmod +x gprofiler sudo ./gprofiler -cu — token “” — service-name “Service name”
Как только установка будет завершена, она начнет работать в фоновом режиме, и вы сможете просматривать статистику своего кода на панели инструментов:
Когда мы проводим профилирование с использованием модулей Python, оно показывает нам только статистические данные о программе. С другой стороны, gProfiler предоставляет изрядный объем данных, таких как процессы, выполняемые на уровне ядра, ресурсы, используемые ЦП, и многое другое. Благодаря этому разработчик полностью осведомлен о том, сколько ресурсов было потреблено и какие функции необходимо оптимизировать.
Заключение
Непрерывное профилирование необходимо для обеспечения правильного использования ресурсов. Профилирование позволяет нам идентифицировать некоторые из самых основных узких мест в коде. Их решение может привести к значительной оптимизации кода и, в конечном итоге, к снижению затрат для компании.
Источник: questu.ru
Ускорение цикла
вроде к LU или qr это не имеет отношения, по крайней мере я на это не ориентировался. Это кусок просто чтобы на двумерной сетке fx,fy, прогнав ее по одномерной сетке nu получить трехмерный массив.
10 сен 2020 в 17:36
Почитайте мой ответ. Ваш процесс не сходится.
10 сен 2020 в 19:01
3 ответа 3
Сортировка: Сброс на вариант по умолчанию
Ускорение в 430 раз
Задача поставлена некорректно
Мне удалось добиться решения за 0.07 секунды с помощью оптимизации вычислений и алгоритмизации.
Сразу следует отметить, что данный процесс не сходится и для различных оптимизаций мы можем получить очень сильные вариации. Это связано с неустойчивостью численного решения, который приводит автор. В частности, для конечного решения это означает, что математические действия над данным выражениям могут носить фатальный характер и конечный результат будет очень сильно меняться. Пример детальный разбор представлен последним пунктом.
Во-первых, перенесите начало отсчёта на момент «перед циклом» (в вашем случае, это ничего не изменит, конечно, но, всё-таки)
import numpy as np import scipy.constants import time c = scipy.constants.c #speed of light N = 1024 Nx = 128 Ny = 128 x_size = 50 y_size = 50 U = np.zeros(shape=(N, Nx, Ny), dtype=complex) fx=np.linspace( — Nx / ( 2 * x_size ), Nx / ( 2 * x_size ), Nx ) fy=np.linspace( — Ny / ( 2 * y_size ), Ny / ( 2 * y_size ), Ny ) nu=np.linspace( — x_size, x_size, N ) start_time = time.time() for k in range( N ): for i in range( Nx ): for j in range( Ny ): U[k, i, j] = 1 — ((c * fx[i] / nu[k]) ** 2) — ((c * fy[j] / nu[k]) ** 2 ) print(«— %s seconds —» % (time.time() — start_time))
Всё сильно зависит от вашего процессора. Например, у меня получается 30 секунд.
1024 * 128 * 128 — это не то чтобы очень мало. 16 млн. Кажется, что 30 секунд вполне разумная цена. Более того, внутри цикла, у вас есть довольно тяжёлые операции
Если хотите ускорения, то давайте оптимизируем цикл:
c2 = c ** 2 for k in range( N ): nu_k2 = nu[k] ** 2 for i in range( Nx ): fx_i = fx[i] ** 2 for j in range( Ny ): U[k, i, j] = 1 — c2 / nu_k2 * (fx_i + ((fy[j]) ** 2 ))
Основная оптимизация происходит за счёт того, что мы не будем повторно вычислять значения. Таким образом, после того, как мы вынесли nu_k2 и fx_i , получаем 18 секунд.
Откажемся от деления на каждом шаге:
c2 = c ** 2 for k in range( N ): nu_k2 = nu[k] ** 2 d = c2 / nu_k2 for i in range( Nx ): fx_i = fx[i] ** 2 for j in range( Ny ): U[k, i, j] = 1 — d * (fx_i + (fy[j]) ** 2 )
и получим прирост ещё в 1-2 секунды.
Так как numpy написан на C , то можем сделать следующее:
fx=np.linspace( — Nx / ( 2 * x_size ), Nx / ( 2 * x_size ), Nx ) ** 2 fy=np.linspace( — Ny / ( 2 * y_size ), Ny / ( 2 * y_size ), Ny ) ** 2 nu=np.linspace( — x_size, x_size, N ) c2 = c ** 2 for k in range( N ): nu_k2 = nu[k] ** 2 d = c2 / nu_k2 for i in range( Nx ): for j in range( Ny ): U[k, i, j] = 1 — d * (fx[i] + fy[j])
Сократим расходы до 13 секунд.
6 (0.07 секунды)
Финальное решение. Вынесем общую подматрицу в предподсчёт и будем её переиспользовать. И, наконец, 0.07 секунды
a = np.zeros((fx.shape[0], fy.shape[0])) for i in range( Nx ): for j in range( Ny ): a[i, j] = fx[i] + fy[j] c2 = c ** 2 for k in range( N ): nu_k2 = nu[k] ** 2 d = c2 / nu_k2 U[k, :, :] = 1 — d * a
Запишем проверку, которая покажет, что решения идентичные
import numpy as np import scipy.constants c = scipy.constants.c #speed of light N = 8 Nx = 4 Ny = 4 x_size = 50 y_size = 50 U = np.zeros(shape=(N, Nx, Ny), dtype=complex) fx=np.linspace( — Nx / ( 2 * x_size ), Nx / ( 2 * x_size ), Nx ) fy=np.linspace( — Ny / ( 2 * y_size ), Ny / ( 2 * y_size ), Ny ) nu=np.linspace( — x_size, x_size, N ) for k in range( N ): for i in range( Nx ): for j in range( Ny ): U[k, i, j] = 1 — ((c * fx[i] / nu[k]) ** 2) — ((c * fy[j] / nu[k]) ** 2 ) U0 = np.zeros(shape=(N, Nx, Ny), dtype=complex) c2 = c ** 2 fx=np.linspace( — Nx / ( 2 * x_size ), Nx / ( 2 * x_size ), Nx ) ** 2 fy=np.linspace( — Ny / ( 2 * y_size ), Ny / ( 2 * y_size ), Ny ) ** 2 nu=c2 / np.linspace( — x_size, x_size, N ) ** 2 a = np.zeros((fx.shape[0], fy.shape[0])) for i in range( Nx ): for j in range( Ny ): a[i, j] = fx[i] + fy[j] for k in range(N): for i in range( Nx ): for j in range( Ny ): U0[k, :, :] = 1 — nu[k] * a print(abs(U0 — U).sum())
Норма равно 0.01
Далее, более формально можно привести к матричному виду и ещё ускорить. Но это уже совсем другая задача.
Анализ сходимости
Рассмотрим небольшие начальные данные. Сделаем простое преобразование. В результирующей формуле вынесем минус за скобки. Тогда получим выражение:
1 — ((c * fx[i] / nu[k]) ** 2) + ((c * fy[j] / nu[k]) ** 2 )
Итоговый код, который сравивает получающиеся решения:
import numpy as np import scipy.constants import time c = scipy.constants.c #speed of light N = 64 Nx = 32 Ny = 32 x_size = 50 y_size = 50 U = np.zeros(shape=(N, Nx, Ny), dtype=complex) fx=np.linspace( — Nx / ( 2 * x_size ), Nx / ( 2 * x_size ), Nx ) fy=np.linspace( — Ny / ( 2 * y_size ), Ny / ( 2 * y_size ), Ny ) nu=np.linspace( — x_size, x_size, N ) for k in range( N ): for i in range( Nx ): for j in range( Ny ): U[k, i, j] = 1 — ((c * fx[i] / nu[k]) ** 2) — ((c * fy[j] / nu[k]) ** 2 ) U0 = np.zeros(shape=(N, Nx, Ny), dtype=complex) fx=np.linspace( — Nx / ( 2 * x_size ), Nx / ( 2 * x_size ), Nx ) fy=np.linspace( — Ny / ( 2 * y_size ), Ny / ( 2 * y_size ), Ny ) nu=np.linspace( — x_size, x_size, N ) for k in range( N ): for i in range( Nx ): for j in range( Ny ): x = ((c * fx[i] / nu[k]) ** 2) + ((c * fy[j] / nu[k]) ** 2 ) U0[k, i, j] = 1 — x print(abs(U0 — U).sum())
Если мы посмотрим на результат, то заметим, что исходное решение и предложенное отличается на 1058 . Таким образом, делаем вывод, что данный процесс не сходится. И здесь вопрос к автору. Что это за задача
Отслеживать
ответ дан 9 сен 2020 в 15:30
hedgehogues hedgehogues
9,271 8 8 золотых знаков 45 45 серебряных знаков 93 93 бронзовых знака
можно ещё заранее получить range(N) , range(Nx) , range(Ny) . прирост минимальный, но тоже немного оптимизация 😉
9 сен 2020 в 15:40
9 сен 2020 в 15:47
время на создание range крошечное, но факт — создание range будет чуть дольше, чем использование готового, т.к. у нас очень много раз повторяется цикл, то немного это ускорит процесс +- 4 секунды, если учитывать создание всех range (1024 * 128 * 128 + 1024 * 128 + 1024)
9 сен 2020 в 15:55
проверь с созданием и использованием range , раз уж идём до конца
9 сен 2020 в 16:00
0.07 без всех махинаций. Просто преобразорваниями
9 сен 2020 в 16:01
- Зачем тип complex ?? Не хочешь перейти на float ? Тогда вычисления будут побыстрее )
- Оптимизируй цикл. Попробуй так:
import numpy as np import scipy.constants import time start_time = time.time() c = scipy.constants.c #speed of light N = 1024 Nx = 128 Ny = 128 x_size = 50 y_size = 50 U = np.zeros(shape=(N, Nx, Ny), dtype=complex) c_fx_i_div_nu_k = np.zeros(shape=(N, Nx), dtype=complex) c_fy_j_div_nu_k = np.zeros(shape=(N, Ny), dtype=complex) fx=np.linspace( — Nx / ( 2 * x_size ), Nx / ( 2 * x_size ), Nx ) fy=np.linspace( — Ny / ( 2 * y_size ), Ny / ( 2 * y_size ), Ny ) nu=np.linspace( — x_size, x_size, N ) c_div_nu = c / nu for k in range( N ): for i in range( Nx ): c_fx_i_div_nu_k[k,i] = (c_div_nu[k] * fx[i])**2 for k in range( N ): for j in range( Ny ): c_fy_j_div_nu_k[k,j] = (c_div_nu[k] * fy[j])**2 for k in range( N ): for i in range( Nx ): for j in range( Ny ): U[k, i, j] = 1 — c_fx_i_div_nu_k[k,i] — c_fy_j_div_nu_k[k,j] print(«— %s seconds —» % (time.time() — start_time))
так более чем в 2 раза быстрее.
а насколько быстрее хочется?
в 10 раз?
тогда надо подумать о вычислениях на видеокарте или при помощи, многопоточности numba
Вот пример с float + numba
но — проверь сам. Правильно ли посчиталось. ))
numba — она такая.
На моем стареньком компьюторе получилось:
— 1.2685229778289795 seconds —
Что в 85 раз быстрее исходного варианта
Источник: ru.stackoverflow.com
Почему Python такой медленный и как его ускорить
В этой статье мы узнаем, что Python — неплохой язык, который просто очень медленный. Он оптимизирован для своей цели: простой синтаксис, читаемый код и большая свобода для разработчика. Однако эти варианты дизайна делают код Python медленнее, чем другие языки, такие как C и Java.
Понимание того, как Python работает «под капотом», покажет нам, почему он работает медленнее. Как только причины будут ясны, мы сможем их обойти. Прочитав эту статью, вы будете иметь четкое представление о:
- как устроен и работает Python
- почему эти варианты дизайна влияют на скорость выполнения
- как мы можем обойти некоторые из этих узких мест, чтобы значительно увеличить скорость нашего кода
Эта статья разделена на три части. В части A мы рассмотрим, как устроен Python. Затем в части Bузнайте, как и почему эти варианты дизайна влияют на скорость. Наконец, в части C мы узнаем, как обойти узкие места, возникающие в результате дизайна Python, и как мы можем значительно ускорить наш код.
Поехали!
Часть A — Дизайн Python
Начнем с определения. Википедия описывает Python как:
Python — это интерпретируемый язык программирования высокого уровня общего назначения. Он динамически типизируется и очищается от мусора.
Хотите верьте, хотите нет, но вы поймете два приведенных выше предложения после того, как прочитаете эту статью. Это определение дает хорошее представление о дизайне Python. Высокоуровневая, интерпретируемая, универсальная, динамическая типизация и способ сбора мусора избавляют разработчика от многих хлопот.
В следующих частях мы рассмотрим эти элементы дизайна, объясним, что они означают для производительности Python, и закончим практическим примером.
Медлительность против ожидания
Сначала давайте поговорим о том, что мы пытаемся измерить, когда говорим «медленно». Ваш код может работать медленно по множеству причин, но не во всех из них виноват Python. Допустим, есть два типа задач:
Примерами задач ввода-вывода являются запись файла, запрос некоторых данных из API, печать страницы; они предполагают ожидание. Хотя они заставляют вашу программу выполняться дольше, это не вина Python. Он просто ждет ответа; более быстрый язык не может ждать быстрее. Этот вид медлительности не то, что мы пытаемся решить в этой статье. Как мы увидим позже, мы можем связать эти типы задач (также описано в этой статье).
В этой статье мы выясняем, почему Python выполняет процессорные задачи медленнее, чем другие языки.
Динамически типизированный против статически типизированного
Python имеет динамическую типизацию. В таких языках, как C, Java или C++, все переменные имеют статический тип, это означает, что вы записываете конкретный тип переменной, такой как int my_var = 1; .
В Python мы можем просто ввести my_var = 1 . Затем мы можем даже присвоить новое значение совершенно другого типа, например my_var = “a string» . Мы увидим, как это работает внутри, в следующей главе.
Хотя динамическая типизация довольно проста для разработчика, у нее есть серьезные недостатки, как мы увидим в следующих частях.
Скомпилированные и интерпретированные
Компиляция кода означает взять программу на одном языке и преобразовать ее на другой язык, как правило, более низкого уровня, чем исходный код. Когда вы компилируете программу, написанную на C, вы конвертируете исходный код в машинный код (который является фактическими инструкциями для ЦП), после чего вы можете запустить свою программу.
Python работает немного иначе:
- Исходный код компилируется не в машинный код, а в независимый от платформы байт-код. Как и машинный код, байт-код также является инструкциями, но вместо того, чтобы выполняться процессором, он выполняется интерпретатором.
- Исходный код компилируется во время работы. Python компилирует файлы по мере необходимости, а не компилирует все перед запуском программы.
- Интерпретатор анализирует байт-код и переводит его в машинный код.
Python должен компилироваться в байт-код, потому что он динамически типизирован. Поскольку мы не указываем тип переменной заранее, нам нужно дождаться фактического значения, чтобы определить, является ли то, что мы пытаемся сделать, действительно допустимым (например, сложение двух целых чисел), прежде чем переводить в машинный код. Это то, что делает интерпретатор. В статически типизированных компилируемых языках компиляция и интерпретация выполняются до запуска кода.
Подводя итог: код замедляется из-за компиляции и интерпретации, которые происходят во время выполнения. Сравните это со статически типизированным скомпилированным языком, который после компиляции выполняет только инструкции ЦП.
На самом деле можно расширить Python с помощью скомпилированных модулей, написанных на C. Эта статья и эта статьядемонстрирует, как вы можете написать собственное расширение на C, чтобы ускорить свой код x100 .
Сборка мусора и управление памятью
Когда вы создаете переменную в Python, интерпретатор автоматически выбирает место в памяти, достаточно большое для значения переменной, и сохраняет его там. Затем, когда переменная больше не нужна, слот памяти снова освобождается, чтобы другие процессы могли его снова использовать.
В C, языке, на котором написан Python, этот процесс вообще не автоматизирован. Когда вы объявляете переменную, вам нужно указать ее тип, чтобы можно было выделить правильный объем памяти. Также сборка мусора производится вручную.
Так как же Python отслеживает, какую переменную нужно собирать? Для каждого объекта Python отслеживает, сколько объектов ссылается на этот объект. Если счетчик ссылок переменной равен 0, мы можем сделать вывод, что переменная не используется и что ее можно освободить в памяти. Мы увидим это в действии в следующей главе.
Однопоточный против многопоточного
Некоторые языки, такие как Java, позволяют выполнять код параллельно на нескольких процессорах. Python, однако, изначально является однопоточным на одном процессоре. Механизм, обеспечивающий это, называется GIL: Global Interpreter Lock. GIL гарантирует, что интерпретатор выполняет только один поток в любой момент времени.
Проблема, которую решает GIL, заключается в том, как Python использует подсчет ссылок для управления памятью. Счетчик ссылок переменной должен быть защищен от ситуаций, когда два потока одновременно увеличивают или уменьшают счетчик. Это может вызвать всевозможные странные баги, вплоть до утечек памяти (когда объект больше не нужен, но не удаляется) или, что еще хуже, неправильное освобождение памяти. В последнем случае переменная удаляется из памяти, в то время как другие переменные все еще нуждаются в ней.
Вкратце: из-за того, как спроектирована сборка мусора, Python должен реализовать GIL, чтобы гарантировать, что он работает в одном потоке. Однако есть способы обойти GIL, прочитайте эту статью, организовать многопоточную или многопроцессорную обработку вашего кода и значительно ускорить его.
Часть B. Заглянем внутрь: дизайн Python на практике
Хватит теории, давайте посмотрим на действие! Теперь, когда мы знаем, как устроен Python, давайте посмотрим на него в действии. Мы сравним простое объявление переменной как в C, так и в Python. Таким образом, мы можем увидеть, как Python управляет своей памятью и почему его выбор дизайна приводит к более медленному времени выполнения по сравнению с C.
Объявление переменной в C
Начнем с объявления целого числа в C с именем c_num.
int c_num = 42;
Когда мы выполняем эту строку кода, наша машина делает следующее:
- Выделить достаточно памяти для целого числа по определенному адресу (место в памяти)
- Присвойте значение 42 расположению памяти, выделенной на предыдущем шаге.
- Укажите c_num на это значение
Image Теперь в памяти существует объект, который выглядит так:
Если мы назначаем новый номер c_num, мы записываем новый номер по тому же адресу; перезапись, предыдущее значение. Это означает, что переменная является изменяемой.
Обратите внимание, что адрес (или расположение в памяти) не изменился. Думайте об этом как о c_numвладении фрагментом памяти, достаточно большим для целого числа. В следующей части вы увидите, что это отличается от того, как работает Python.