Как нагрузить процессор python

Обновлено: 01.07.2024

Я использую Python для выполнения некоторых алгоритмов, и независимо от того, какой Python я использую, и я перепробовал много версий, загрузка ЦП достигает 25%. Почему Python не использует все остальные ресурсы моего процессора? Я изменил приоритет службы с обычного на высокий, а затем на реальное время с перезапусками между ними, но ничего не изменилось.

Есть ли способ заставить Python использовать 50% или даже больше моего процессора?

да, это i5-480M и в панели управления> параметры питания> процессор мин / макс на 100%

Проще говоря, вы запускаете однопоточное приложение в системе с 4 логическими ядрами - поэтому у вас есть один процесс, использующий все ядро.

Вам нужно (и это не тривиально) переписать алгоритм, чтобы он был многопоточным, или посмотреть, сможете ли вы просто запустить 2 или более экземпляра на конкретных ядрах, чтобы использовать больше вашего процессора. Другого пути нет.

Я боялся, что это будет иметь место, но на диаграммах использования процессора в диспетчере задач Windows я не вижу, чтобы 1 диаграмма была на пике во время выполнения алгоритма, напротив, я вижу их все со значительным увеличением. Ваша система балансирует нагрузку между ядрами. Тем не менее, два ядра не используются одновременно. ребята, вы подтвердили мои опасения, кажется, пришло время начать читать о потоках @ fractal_7: многопоточность может не принести ожидаемых результатов. Смотрите мой ответ ниже.

Язык Python предшествует многоядерным процессорам, поэтому не странно, что он не использует их изначально.

Кроме того, не все программы могут получать прибыль от нескольких ядер. Вычисление, выполненное поэтапно, где следующий шаг зависит от результатов предыдущего шага, не будет быстрее при использовании большего количества ядер. Проблемы, которые можно векторизовать (применяя одни и те же вычисления к большим массивам данных), можно относительно легко использовать с использованием нескольких ядер, поскольку отдельные вычисления независимы.

Когда вы делаете много расчетов, я предполагаю, что вы используете NumPy ? Если нет, проверьте это. Это расширение, написанное на C, которое может использовать оптимизированные библиотеки линейной алгебры, такие как ATLAS. Это может значительно ускорить численные расчеты по сравнению со стандартным Python.

Сказав это, есть несколько способов использовать несколько ядер с Python.

  • Встроенный multiprocessing модуль. multiprocessing.Pool Класс предоставляет векторизации по нескольким процессорам с map() и связанными с ними методами. Здесь есть компромисс здесь. Если вам приходится передавать большие объемы данных между процессами, то эти издержки могут свести на нет преимущество нескольких ядер.
  • Используйте подходящую сборку NumPy. Если numpy построен с использованием многопоточной библиотеки ATLAS, это будет быстрее при больших проблемах.
  • Используйте модули расширения, такие как NumberxPr , параллельный Python , CorePy или Copenhagen Vector Byte Code .

Обратите внимание, что threading модуль не так уж полезен в этом отношении. Для простоты управления памятью глобальная блокировка интерпретатора («GIL») обеспечивает выполнение только одного потока за раз байт-кодом Python. Внешние модули, такие как numpy, могут использовать несколько потоков внутри.

Совсем недавно вышла новая версия 0.34 библиотеки оптимизирующего JIT компилятора Numba для Python. И там ура! появилась долгожданная семантика аннотаций и набор методов для организации параллельных вычислений. За основу была взята технология Intel Parallel Accelerator.

В данной статье я хочу поделиться результатами первого тестирования скорости вычислений на основе этой библиотеки для некоторой современной машины с четырехядерным процессором.

Введение

В настоящее время Python очень активно используется в научных вычислениях, а в области Machine Learning вообще является практически одним из стандартов. Но если посмотреть чуть глубже, то почти везде Python используется как обертка над библиотеками более низкого уровня, написанных большей частью на C/C++. А можно ли на чистом Python писать на самом деле быстрый и параллельный код?

Рассмотрим совсем простую задачу. Пусть нам даны два набора N точек в трехмерном пространстве: p и q. Необходимо вычислить специальную матрицу на основе попарных расстояний между всеми точками:


Для всех тестов возьмем N = 5000. Время вычисления усредняется для 10 запусков.

Реализация на C++

Как точку отсчета возьмем следующую реализацию на C++:


Внешний цикл по точкам p выполняется параллельно с использованием технологии OpenMP.

Время выполнения: 44 мс.

Чистый Python

Начнем тест скорости с кода на чистом Python:


Время выполнения: 52 861 мс, медленнее базовой реализации больше, чем в 1000 раз.

Python интерпретируемый язык, Python использует внутри себя GIL, что делает невозможным параллелизацию на уровне самого кода. Это все очень медленно. Теперь начнем это все ускорять.

Python + NumPy + SciPy

Проблему медленности Python для численных задач осознали очень давно. И ответом на эту проблему была библиотека NumPy. Идеология NumPy во многом близка MatLab, который является общепризнанным инструментом научных расчетов.

Мы прекращаем мыслить итерационно, мы начинаем мыслить матрицами и векторами как атомарными объектами для вычисления. А все операции с матрицам и векторами на нижнем уровне уже выполняются высокопроизводительными библиотеками линейной алгебры Intel MKL или ATLAS.

Реализация на NumPy выглядит так:


В этой реализации вообще нет ни одного цикла!

Время выполнения: 839 мс, что медленнее базовой реализации где-то в 19 раз.

Более того, в NumPy и SciPy есть огромное количество встроенных функций. Реализация данной задачи на SciPy выглядит так:


Время выполнения: 353 мс, что медленнее базовой реализации в 8 раз.

Для большинства задач это уже вполне приемлемое время работы. Ценой этого является переключение способа мышления, теперь необходимо собирать код из базовых операций линейной алгебры. Иногда это выглядит очень красиво, но порой приходится придумывать разные трюки.

Но а как быть с параллельностью? Здесь она неявная. Мы надеемся, что на низком уровне все операции с матрицами и векторами реализованы эффективно и параллельно.

А что делать, если наш код не вписывается в линейную алгебру, или мы хотим явную параллелизацию?

Python + Cython

Тут приходит время Cython. Cython это специальный язык, который позволяет внутри обычного Python кода вставлять код на C-образом языке. Далее Cython преобразует это код в .c файлы, которые компилируется в python модули. Эти модули достаточно прозрачно можно вызывать в других частях Python кода. Реализация на Cython выглядит так:


Что тут происходит? На вход функция принимает python numpy объекты, далее они преобразуются в типизированные Cython С-структуры, а далее отключается gil и при помощи специальной конструкции 'prange' внешний цикл выполняется параллельно.

Время выполнения: 76 мс, что в 1.75 раз медленнее, чем базовая реализация.

Ну что, мы почти приблизились к базовой реализации, мы начали писать явный параллельный код. Но ценой этого стал менее читаемый код, мы ушли от чистого Python.

В целом, большинство численных расчетов так и пишутся. Большая часть на NumPy, а некоторые места критичные по быстродействию выносятся в отдельные модули и реализуются на cython.

Python + Numba

Мы проделали длинный путь. Мы начали с чистого Python, потом пошли по пути магии матричных вычислений, потом погрузились в специальных язык расширений. Пора вернуться обратно к тому, с чего мы начали. Итак, реализация на Python + Numba:


Время выполнения: 46 мс, что практически совпадает с базовой реализацией!

И все, что нужно было сделать для этого с исходным медленным Python кодом:


Скорость работы приложений в продакшене — всегда горячая тема. Как, не нагружая сервер на все сто процентов и не мешая работе пользователей, узнать, насколько быстро или медленно все работает? Запустив легкий профилировщик, мы легко получим ответы на эти вопросы.

Когда цена за гигабайт памяти составляет копейки (

Профилирование vs тестирование производительности

Итак, как нам найти узкие места в нашем софте? Существуют всего два способа (вариант «пользователи не жалуются — значит, все хорошо» я не беру в расчет): performance-тестирование и профилирование (англ. profiling). Тестировать производительность можно, например, с целью проверить, что твое приложение будет работать под определенной нагрузкой, или понять, насколько быстрее новая версия по сравнению с предыдущей.

К сожалению, перформанс-тестирование не всегда дает нужные результаты. Как правило, тестирование производительности выполняется до момента запуска ПО в продакшен. А это значит, что оно основывается (или как минимум должно основываться) на нефункциональных требованиях к разрабатываемому софту. Но мир далек от совершенства, и часто после релиза твою программу/сервис/веб-приложение начинают использовать не так, как ты этого ожидал. Правило Парето еще никто не отменял, и если ты оптимизировал не те 20% своего кода, то тебе придется начинать все по новой. Но теперь уже с учетом того, что будут делать пользователи.

В отличие от перформанс-тестов, цель профилирования — собрать характеристики работы программы для дальнейшей оптимизации.

Как видишь, цели разнятся достаточно сильно. Но это и неудивительно — для каждой задачи должен быть свой инструмент, и швейцарский нож не всегда подходит.


Профилирование интересно еще и тем, что его часто, хотя и не всегда (зависит от типа конкретного профайлера), можно применять прямо на боевом сервере, так как профайлеры практически не влияют на производительность — потерять 1–2% производительности сервера не так страшно, как проиметь >50–70% ресурсов на перформанс-тесты. Тем более что такие компании, как Dropbox, успешно это делают. Из плюсов такого подхода можно отметить следующее:

Оговорюсь сразу, что на производительность ПО влияет уж очень много факторов, поэтому полагаться исключительно на цифры не стоит. Я рекомендую все же вычислять какие-то средние значения — так будет более объективно. Иначе разброс цифр получится очень большой.

Например, на производительность веб-приложения могут влиять как неоптимальный запрос к БД, так и настройки кеширования в файловой системе. И это если отбросить то, что даже на одинаковых на первый взгляд серверах скорость работы HDD/SSD/RAM может отличаться. Да, это будут какие-то миллисекунды, но в масштабах десятка тысяч посетителей в день/час/минуту это может оказаться более чем существенно. Я уже не говорю про настройку СУБД, веб-серверов, сети и прочее — это потянет на отдельную статью.

Итак, вернемся к нашей проблеме — профилированию Python-приложений в production. В качестве профилировщика я выбрал plop — простой и эффективный статистический профайлер от одного из авторов Tornado, который использует у себя в продакшене Dropbox. Профайлер настолько прост и универсален, что можно работать практически с любой программой, написанной на Python. Особо хочется отметить, что у него нет зависимостей — а это сильно расширяет возможности по его использованию. Да, в репозитории проекта есть requirements.txt с одной лишь строчкой — tornado, но это нужно только для того, чтобы просматривать отчет.

Установка plop’а

К сожалению, на момент написания статьи я не нашел публичных репозиториев с DEB- и RPM-пакетом для plop’а. Это может быть аргументом против его использования в продакшене, но он отлично работает в virtualenv, и всегда можно найти компромисс с администратором и/или devops-инженером, ответственным за установку ПО на сервер.

Особенности работы статистического профайлера

Принцип работы любого статистического профайлера, в том числе и plop’а, выглядит примерно так.

Раз в N-й отрезок времени профайлер смотрит, какая функция программы работает в данный момент, и записывает эту информацию. В идеале информацию профайлер должен собирать как можно дольше, иначе данные будут очень неточными. Также статистические профайлеры могут не успевать логировать данные в функциях, которые не работают значительно быстрее N; для этого следует собирать данные о работе программы как можно дольше и, при возможности, выполнять одно и то же действие несколько раз. Таким образом, статистический профайлер может показать не всю информацию о работе исследуемой программы, но проблемные места он обязательно выявит.

Продукт еще сыроват, но автор достаточно быстро принимает патчи с помощью pull-request’ов на GitHub. Я бы сказал, что plop прекрасно справляется со своей первоначальной задачей — сбором метрики, а вот над обработкой данных нужно еще поработать — в итоговом отчете очень много лишней информации. Таким образом, мы не только получаем отчет о своей программе, но и видим, что и сколько раз вызывается во всех библиотеках, которые используются. Но plop может генерировать данные в JSON, а как его обрабатывать — это уже задача не профайлера. Некоторые могут назвать недостатком то, что у plop’а нет документации. Но для читателей Х это не должно быть проблемой: во-первых, там есть минимально необходимая документация по использованию и запуску, а во-вторых, разобраться в нескольких сотнях строчек кода не составит труда даже начинающему Python-программисту.

Принцип работы plop’а

При дефолтных настройках plop работает следующим образом.

На протяжении 30 с (duration) создается таймер, который каждые 10 мс останавливает выполнение программы, записывает ее текущие stack trace с помощью функции sys._current_frames(). Параметр duration при запуске plop’а из командной строки по умолчанию будет равен не 30, а целых 3600 с, то есть час! Разница в 120 раз может влиять на точность вычисления, нужно об этом помнить и быть осторожным. То, что профайлер работает не всегда, а только в определенный отрезок времени, очень хорошо сказывается на производительности и позволяет использовать его в продакшене: например, на таком достаточно большом веб-приложении, как OpenStack Cinder API, потеря скорости была всего overhead was 3.27297403843e-05 per sample (0,00327297403843%).

После работы профайлера можно сгенерировать HTML-страницу и посмотреть прямо в браузере красивый отчет, созданный с помощью библиотеки D3.js.

Рис. 1. Plop в действии

Рис. 1. Plop в действии

Сигналы и таймеры

Вся работа plop’а построена на обработке сигналов по таймеру: грубо говоря, каждые X миллисекунд приложение будет получать сигнал Y, по которому оно остановится и запишет данные, необходимые для профайлинга.

Всего в *nix-системах существует три типа таймеров (man setitimer), которые нам доступны. Управляются они следующими сигналами:

  • signal.ITIMER_REAL — уменьшает таймер реального времени;
  • signal.ITIMER_VIRTUAL — уменьшает интервальный таймер только тогда, когда выполняется наш процесс;
  • signal.ITIMER_PROF — то же самое, что и предыдущий, только считается и системное время.

В plop’е по умолчанию используется virtual mode, это означает, что вычисляется только время исполнения питоновского кода. Этот параметр устанавливается или параметром --mode (при запуске из командной строки), или соответствующим аргументом mode в конструкторе класса Collector.

Запуск!

Есть два способа запустить профайлер. Первый способ более общий и позволяет запустить профайлер с любой программой, написанной на Python’е, а второй требует небольшого изменения кода. Рассмотрим оба подробнее.

Запускаем plop без модификации исходников

Тут все очень просто. Мы запускаем на выполнение модуль plop.collector и в качестве аргументов указываем нашу программу:

Все. Теперь после завершения программы будет сгенерирован файл с данными профайлера. По умолчанию он называется /tmp/plop.out. Также plop напишет, сколько времени потратилось на профайлер.

Продвинутый способ запустить профайлер

В предыдущем способе запуска есть один недостаток — профайлер будет работать всегда. Пусть он и не сильно влияет на производительность, но на очень загруженной системе, когда каждая миллисекунда на счету, это может быть ключевым доводом против использования профайлера. Решить эту проблему можно, интегрировав plop в свой проект. Просто добавляем в зависимости строку plop, а далее в коде мы уже сами можем выбирать, когда и как включать и выключать профайлер. Разберем такой вариант подробнее.

Этот способ удобен тем, что его можно включать и выключать в процессе работы приложения без даунтайма, — достаточно всего лишь реализовать нужные REST API. Хотя есть и более удобный способ: так как профайлер, как правило, запускает кто-либо с админскими правами, то нам не надо давать для этого REST API, пусть даже только админу, — нужно только реализовать поддержку сигнала SIGHUP и менять значение в конфиге. При получении сигнала SIGHUP приложения, как правило, перечитывают свои конфиги без перезагрузки. Этот способ будет работать только при условии, что есть доступ к консоли сервера, на котором крутится наше приложение.

Вся «магия» профайлера собрана в классе Collector. Как несложно догадаться, для запуска профайлера нам надо вызвать метод start и, соответственно, метод stop для остановки. Поэтому вам необходимо всего лишь создать экземпляр класса Collector и в нужное время включить и/или выключить его. После чего сохранить собранные данные в специально отведенном для этого месте — и можно смотреть на результаты профайлера.

В мире кругов

Итак, с конфигурацией и запуском мы разобрались, осталось разобраться, как работать с данными профайлера. И если в результатах работы профайлера простого приложения все легко и понятно, то стоит только подключить его к чему-нибудь, содержащему что-то вроде Flask или Django. В общем, сначала выдача кажется грандиозной, но потом ты внезапно осознаешь, что ничего в ней не понимаешь.

Plop строит дерево вызовов всех функций, включая импортирование модулей. Размер кругов показывает, как много времени было потрачено на функцию, а чем толще стрелка, тем чаще ее вызывали. Стоит отметить, что на графике самый большой круг и, соответственно, самое большое время будет занимать сам профайлер, но на практике это число очень маленькое.

Рис. 2. График импортирования модулей

Рис. 2. График импортирования модулей

Визуализация очень хорошо показывает, сколько (не)нужных импортов у нас выполняется при работе программы (см. рис. 2). Очень много данных будет поступать от использованных фреймворков (SQLAlchemy, Django и так далее), поэтому имеет смысл с помощью plop viewer’а получить нужные данные в формате JSON и обработать их, удалив записи о профайлинге, например SQLAchemy. Делается это так:

Такой нехитрый JavaScript покажет нам исключительно данные по нужным нам файлам, но при этом ломает дерево вызовов (рис. 3).

Рис. 3. График времени работы

Рис. 3. График времени работы

В случае же нехватки данных мы не увидим разницу в выполнении методов и функций на диаграмме (рис. 4).

Рис. 4. Профайлер считает, что все функции работают одинаково

Рис. 4. Профайлер считает, что все функции работают одинаково

Подводя итоги

Если говорить кратко, то профилирование приложений с использованием статистического профайлера дает не слишком детальную картину происходящего, но практически не влияет на производительность. Смотреть на результаты такого профайлера нужно внимательно, и будь готов к тому, что результаты могут оказаться неточными, а для их сбора может понадобиться немало времени. У каждого инструмента есть свои плюсы и минусы и, самое главное, сфера применения. Поэтому выбирай инструмент, который подходит конкретно для поставленной задачи. Хорошего профайлинга и быстрого софта!

Я использую Python для выполнения некоторых алгоритмов и независимо от того, какой Python я использую, и я пробовал много версий, использование процессора идет до 25% максимум. Почему Python не использует остальные ресурсы процессора? Я изменил приоритет службы с нормального на высокий, а затем на реальное время, с перезапусками между ними, но ничего не изменилось.

есть ли способ заставить Python использовать 50% или даже больше моего процессора ?

проще говоря, вы запускаете однопоточное приложение в системе с 4 логическими ядрами-таким образом, у вас есть один процесс, использующий все ядро.

вам (и это нетривиально) нужно переписать алгоритм, чтобы он был многопоточным, или посмотреть, можете ли вы просто запустить 2 или более экземпляров на определенных ядрах, чтобы использовать больше вашего процессора. Другого пути нет.

язык Python предшествует многоядерным процессорам, поэтому не странно, что он не использует их изначально.

кроме того, не все программы могут получать прибыль от нескольких ядер. Вычисление, выполняемое шагами, где следующий шаг зависит от результатов предыдущего шага, не будет выполняться быстрее, если используется больше ядер. Проблемы, которые могут быть векторизовать (прикладывающ такое же вычисление к большим массивам данных) можно относительно легко сделать для использования множественных Ядров потому что индивидуал вычисления независимы.

когда вы делаете много расчетов, я предполагаю, что вы используете включает в себя? Если нет, то проверьте его. Это расширение, написанное на C, которое может использовать оптимизированные библиотеки линейной алгебры, такие как ATLAS. Это может значительно ускорить численные расчеты по сравнению со стандартным python.

сказав это, есть несколько способов использовать несколько ядер с python.

  • встроенный-это multiprocessing модуль. Тот multiprocessing.Pool класс обеспечивает векторизацию между несколькими процессорами с помощью map() и связанных с ним методов. Однако здесь есть компромисс. Если необходимо передавать большие объемы данных между процессами, это может свести на нет преимущества нескольких ядер.
  • используйте подходящую сборку numpy. Если numpy построен с многопоточной библиотекой ATLAS, он будет быстрее при больших проблемах.
  • использовать модули расширения, такие как numexpr,параллельно python,corepy или Копенгаген Векторный Байтовый Код.

отметим, что threading модуль не все, что полезно в этом отношении. Чтобы упростить управление памятью, глобальная блокировка интерпретатора ("GIL") обеспечивает выполнение байт-кода python только одним потоком одновременно. Однако внешние модули, такие как numpy, могут использовать несколько потоков внутри.

Читайте также: