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

A screenshot of a system monitor app showing 24 cores, half of which are at 100% utitilization and half of which are close to 0%.
Например, эта машина потребляет 50% ресурсов CPU, поэтому, вероятно, способна выполнять вдвое больше своих задач.

Однако когда владельцы пытаются реально проецировать эти значения, то оказывается, что процент использования CPU на самом деле растёт не совсем линейно. Но насколько непрямой может быть зависимость?

Чтобы ответить на этот вопрос, я выполнил кучу стресс-тестов, мониторя при этом объём выполняемых ими работы и отображаемый системой уровень использования CPU, а затем по результатам построил графики.

Подготовка

В качестве тестовой машины я использовал десктоп Ubuntu с процессором Ryzen 9 5900X (12 ядер / 24 потока). Также я включил Precision Boost Overdrive (то есть Turbo).

При помощи вайб-кодинга я написал скрипт, выполняющий в цикле stress-ng, сначала с использованием 24 воркеров, попробовав выполнять их каждый при разных уровнях нагрузки на CPU, от 1% до 100%, а затем использовал от 1 до 24 при нагрузке 100%. Тест использовал разные методики нагрузочного тестирования и измерял количество операций, которые можно было выполнить («Bogo ops1»).

Я проводил две методики тестирования, потому что операционные системы умно используют свой планировщик, поэтому планирование небольшого количества воркеров с использованием процессора на 100% можно реализовать оптимально, но когда все 24 воркера находятся на уровне использования 50%, операционной системе сложно сделать что-то иное, нежели распределить работу равномерно.

Результаты

Сырые результаты в CSV можно посмотреть здесь: https://docs.google.com/spreadsheets/d/1QKdYa3NIFGTixG_LdnsbwbeLDnE2GNVVsS9-dfkcT20/edit?usp=sharing.

Общий тест CPU

Самый простой тест просто выполняет в цикле все стресс-тесты CPU утилиты stress-ng.

Как мы видим, когда система сообщает об использовании CPU на 50%, на самом деле он выполняет 60-65% от реальной максимальной работы.

64-битная целочисленная математика

Хм, возможно, это была случайность? Что, если просто выполнять какие-то произвольные вычисления с 64-битными integer?

Здесь картина ещё печальнее! При «использовании на 50%» мы на самом деле выполняем 65-85% от максимальной работы. Ну, хуже ведь уже быть не может?

Матричные вычисления

Что-то тут определённо не так. При выполнении матричных вычислений «использование на 50%» — это, на самом деле, от 80% до 100% максимально возможного объёма работы.

На скриншоте с монитором системы из начала статьи показан тест матричных вычислений с 12 воркерами; можно увидеть, что он сообщал об использовании на 50%, несмотря на то, что дополнительные воркеры абсолютно ничего не делают (за исключением повышения значений использования ресурсов CPU).

Бонус: Nginx

На Hacker News мне предложили провести реальный бенчмарк, поэтому я прогнал бенчмарк Nginx из Phoronix Test Suite, ограниченный 1-24 ядрами (к сожалению, я не мог более точно контролировать процент использования CPU, поэтому получился только один график).

Здесь мы видим, что изначально определяемый процент использования ниже реального, а затем ситуация ухудшается. При использовании на 50% на самом деле выполняется 80% от максимального количества запросов в секунду, а при 80% мы на самом деле уже на 100% от максимального объёма запросов.

Что происходит?

Hyperthreading

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

Главная причина происходящего — hyperthreading: половина «ядер» на этой машине (и на большинстве машин) делит ресурсы с другими ядрами. Если я запущу на этой машине 12 воркеров, то каждому из них назначат отдельное физическое ядро без общих ресурсов, но при превышении их количества каждый дополнительный воркер будет делить ресурсы с другим. В некоторых случаях (общие бенчмарки CPU) это лишь немного ухудшает ситуацию, а в других (матричные вычисления с активным применением SIMD) не остаётся ресурсов, которые можно делить.

Turbo

Это сложнее заметить, но Turbo тоже влияет на результаты. При низком уровне использования этот конкретный процессор работает на частоте 4,9 ГГц, но с увеличением количества активных ядер частота медленно снижается до 4,3 ГГц2.

Посмотрите на ось Y. На этом процессоре тактовая частота падает «всего» на 15%.

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

Важно ли это?

Если вы смотрите на уровень использования CPU и предполагаете, что он будет возрастать линейно, то у вас возникнут проблемы. Если вы используете CPU эффективно (показатель использования выше «50%»), то отображаемый уровень использования на самом деле преуменьшен, и иногда существенно.

Имейте также в виду, что я показал результаты только для одного процессора, но производительность hyperthreading и поведение Turbo могут сильно варьироваться для разных моделей процессоров, в особенности от разных производителей (AMD и Intel).

Наилучший известный мне способ частичного решения этой проблемы — прогонять бенчмарки и мониторить выполняемую реальную работу:

  1. Замерять бенчмарком, какой объём работы может выполнять сервер, прежде чем начнут возникать ошибки или нежелательные уровни задержек.

  2. Отслеживать, какой объём работы выполняет сервер на текущий момент.

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


  1. Bogo ops — это, вероятно, отсылка к BogoMIPS, то есть к «фиктивному» («bogus») бенчмарку, выполняемому Linux при запуске для крайне приблизительной оценки производительности CPU.

  2. Одно из основных ограничений работы процессоров — это необходимость достаточно быстрого рассеяния тепла. Когда работает только одно ядро, процессор может дать этому ядру немного свободы в тепловыделении, которое не тратится на другие ядра, чтобы оно работало быстрее, но это невозможно, если работают все ядра. Энергопотребление работает похожим образом и в некоторых средах тоже может быть ограничением (в десктопах обычно такого не бывает, но часто встречается на серверах).

Комментарии (8)


  1. wataru
    05.09.2025 09:29

    Тут не только нелинейность, бывает вообще немонотоность. Вот соптимизировали вы вашу программу, а % загрузки ЦПУ только подрос! Это потому, что ЦПУ сбрасывает частоту по возможности и уже меньший объем работы занимает более значительную часть сильно сжавшегося пирога. Это характерно лишь для некоторых типов работы, но так бывает, например, когда у вас куча слабо загруженных очередей задач да еще и с задержками перед выполнениями.

    Более полезной метрикой оказывается потребляемая мощность процессора.


    1. MountainBug
      05.09.2025 09:29

      А что, если у меня установлен верхний предел энергопотребления, но моя задача настолько ресурсоемкая, что, скажем, выделяя на ее решение 12 из 16 ядер, я уже упираюсь в лимит? Получается, что часть ресурсов то фактически еще свободна, 4 ядра я не занял вообще.


      1. knstqq
        05.09.2025 09:29

        что ж, похоже вам значит такой подход не подходит. У вас мобильная разработка?


  1. l1kus
    05.09.2025 09:29

    В играх примерно такая же проблема: из-за того, что большинство из них не умеет грамотно работать с потоками, может казаться, что при загрузке процессора в 50% у тебя остаётся запас по производительности, но на самом деле программа просто не до конца использует весь ресурс процессора, который может. Таким образом, независимо от загрузки процессора в диспетчере задач, это не покажет реальную производительность процессора, поскольку это просто сумма всех используемых потоков. Поэтому, если у тебя многоядерный процессор, это ещё не значит, что все программы будут использовать все ядра.


    1. MainEditor0
      05.09.2025 09:29

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


      1. NoOne
        05.09.2025 09:29

        Тогда в треугольнике попадаем в угол "дорого"


  1. Okeu
    05.09.2025 09:29

    Мне кажется вы(ну или исходный автор) немного неверно интерпретировали графики. Метрика нагрузки CPU действительно ложная метрика, но у вас она вышла таковой именно из-за того, что % нагрузки считается общим, а 0-11 и 12-23 воркеры друг другу не равны. Не все задачи могут эффективно выполняться на логических потоках процессора. А в статье идет прямо таки упор на это.
    Для меня ложность %% нагрузки чего-либо заключается в том, что может внезапно оказаться, что даже при значениях близким к 100% - можно еще очень даже много полезной работы засунуть в конвеер, и наоборот (реже)
    Это обобщенная сферическая в вакууме размазанная во времени характеристика, которая во многих кейсах совершенно неинформативная, а вот ее АНОМАЛЬНОЕ поведение, в совокупности с прочими параметрами - уже может что-то нам сказать. УПД: ее еще и разные системы по-разному считают. Поэтому она для меня всегда являлась косвенным параметром.


  1. Ivan22
    05.09.2025 09:29

    оказывается одна цифра плохо подходит для описания нагрузки современных процессоров с кучей ядер, потоков, гипертредингов и прочая, и прочая