Как нагрузить расчёт в однопоточной программе C++ до 90-100% на используемом ядре?
Всем привет.
Есть расчётная программа с достаточно тяжёлым расчётом, но рассчёт делается долго не по причине значительной нагрузки, а наоборот - по причине НЕЗНАЧИТЕЛЬНОЙ нагрузки на процессор - около 5-10% во время всего расчёта. Каких-то блокирующих операций вывода в консоль или в файл не выполняется. Только расчёт, алгоритм последовательный. Как можно понять в каком месте программы происходит такое "замедление" работы?
P.S.
Может ли быть такое замедление работы связано, например, с интенсивным запросом/освобождением памяти? (Это личное предположение, не обязательно учитывать его)
P.P.S.
Windows 11, C++11, -O2 включена, а если O2 выключена, то просадка ещё раз в 10-15.
Поскольку ты - писатель на С++. То у тебя есть коробочные инструменты - профилировщики.
Они показывают обычно 1-2 функции в коде, которые создают являются CPU consumers.
Делай профилирование и приходи с фрагментом проблемного кода.
Твоя постановка - нагрузить CPU - это полная чепуха. Представь что ты своему водителю
поставил задачу "жечь побольше бензина". Это примерно такого же уровня полезности совет.
Навскидку можно сказать что у тебя (предположительно!) идет неэффективное использование кешей памяти. Но
я не хочу быть гадалкой. Надо смотреть код.
mayton2019, Профиллировщие говорит, что больше всего cpu потребляется вне программы, поэтому я предположил, что может быть тратится время на работу с памятью:
Alex XYZ, RtlUserThreadStart происходит один раз при запуске потока. Или у вас программа очень быстро отрабатывает и накладные расходы на ее старт вы тут и видите, перевешивающие все остальное, или ваша программа запускает очень много потоков. Возможно, это делает какая-то библиотека. Возможно она криво реализована и поддерживает распаралеливание, но вы попросили работать в один поток.
Wataru, Эта самая параллельность тут роли не играет. Если у меня случается один большой расчёт, то и идёт он в один поток, думаю, что RtlUserThreadStart отображается с учётом того, что он выполняет задачу, а не потому что долго вызывается. Если у меня случается так, что тот же объём данных можно разбить на независмые 20 потоков, то и считаются они в 20 раз быстрее. Ниже написал, что может быть я немного ошибся с оценкой производительности, но в целом меня смущает, что я не вижу загрузки хотя бы по одному ядру на 100% в диспетчере задач. Что работает процесс расчёта, что не работает - нет какого-либо отклонения ни в одном ядре. Однако стоит запустить несколько процессов в параллель, тогда видно, что некоторые ядра нагрузились по 100%, но это если потоков много. Если так в целом судить, то скорее всего может быть я неправильно интерпретирую данные? но в принципе хотелось бы понять, где вообще программа тупит.
Из читаемого в профайлере:
почему 54% на скобке?
mayton2019 В принципе я так и поступил в итоге - стал нагружать данными в несколько потоков, которая стала близка к количеству ядер и тогда загрузка ядер стала заметно подниматься почти до 100%. У меня сложилось впечатление, что при одном потоке операционка так умело перекидывала эту задачу в однопоточном режиме между ядрами, что диспетчер задач даже при работе в быстром режиме отображения нагрузки не успевал отслеживать настоящую нагрузку на одно ядро. А как только не осталось ядер для перебрасывания, тут-то процессор и завым всеми вентиляторами. )))
Так что видимо проблема была просто в некачественном отображении загрузки на ядро в диспетчере задач! В подтверждении этого предположения говорит факт, что профайлер студии писал 4-5% от общей загрузки всего процессора по этой задаче, что примерно и соответствует полной нагрузке одного ядра в 24 ядерной системе, на которой всё это и работает.
Во-первых, вы уверены, что у вас эти 10-15% - это загрузка ядра, а не вcего процессора? Диспетчер задач обычно показывает как 100% полную загрузку всех ядер.
Во-вторых, что у вас там за вычесления? Работа с целыми числами? Float? Всякие векторные инструкции? Точно нет никаких пауз вроде sleep()?
Выделение памяти медленное, да, но это потому что надо много вычислений сделать, чтобы найти какой-же кусок памяти выдать программе.
Самое вероятное место для тормозов - это вывод на экран/в файл. Если вы много отладочной информации выводите, это будет бутылочным горлышком.
Теоретически очень активное обращение к памяти тоже будет тормозить, особенно, если памяти не хватает и идет работа с файлом подкачки.
Уверен, что 10-15% от ядра, программа не распараллелена, точно однопоточное. Пауз sleep точно нет. Работа с библиотекой GMP/MPFR, но, по моему это не важно, т.к. GMP вполне умеет нагружать как надо. Обращение к памяти вполне может быть очень активным, т.к. много чего делается на std::vector. Но действительно ли это обращение так сильно влияет на производительность? Почему само выделение/освобождение памяти проходит так медленно, с учётом того, что оно сама память по объёму в программе не сильно меняется за время расчёта?
Wataru, Мне сейчас пришло осознание, что про 10-15 процентов - это я ошибочно определил, т.к. речь идёт о 2-4% от всех CPU, которых у меня 24 ядра. 2-4% Показывал диспетчер задач:
В профайлере показывает так, но вот Kernel-то меня и смущает? Почему "какой-то" kernel занимает 86%, а программа только 13.5?
Alex XYZ, А питон там откуда? Судя по всему у вас все тормозит из-за накладных расходов на старт потоков. Возможно это кривая реализация интерфейса между питоном и Си. В результате чего каждый вызов сишного кода из питона создает новый поток для выполнения очень простой и короткой операции. Возможно быстрее этот интерфейс сделать никак и нельзя, я тут не специалист.
Если вы действительно используете сишный код из питона, то вам надо засунуть в Си почти всю логику, чтобы было мало вызовов из питона и они работали долго. Если выдавать сишному коду по чуть-чуть работы (например, сложить 2 числа), то это будет дико тормозить.
В результате чего каждый вызов сишного кода из питона создает новый поток для выполнения очень простой и короткой операции.
Точно нет. Если данные удаётся разделить на 20 независимых потоков, то исчитаются они в 20 раз быстрее, даже при делении потоков в Python. В данном случае у меня есть Cишная библиотека, которую я подключаю к Python (такая схема работы, нужно затащить сишную программу через обёртку, это точно не узкое место). На чистых тестах в C++ один поток тоже может считаться также долго, как и через python. Разницы нет. Вся логика уже в C++. Питон только подготавливает данные, а там их не много. Массив на 2000 точек по три [[0.5, 1.1, 0.0], ...] и список индексов примерно такого же размера или меньше. Для питона переварить такое в виде того же процесса подготовки данных для передачи в C++ вообще ни о чём.