Выбор железа под задачу: как читать характеристики
Четырнадцать предыдущих глав объясняли, как устроена машина. Эта — про обратную задачу: есть нагрузка, есть бюджет, есть список конфигураций, надо решить, что покупать или арендовать. Задача выглядит как сравнение таблиц и таблицами не решается.
Причина простая. Спецификация описывает пределы платформы: что она сделает при идеальных условиях. Ваша программа живёт не на пределах, а в той точке, куда её загнало сочетание алгоритма, раскладки данных, компилятора и операционной системы. Между пределом и точкой легко помещается порядок величины. Поэтому строка характеристики хорошо отвечает на вопрос «чего эта машина точно не сможет» и почти никогда — на вопрос «насколько быстрее пойдёт мой код».
Отсюда единственный работающий порядок действий, вокруг которого построена глава:
1. Охарактеризовать нагрузку → в какое узкое место она упирается СЕЙЧАС
2. Проверить, что упор настоящий → не лечится ли он дешевле в коде
3. Прочитать спецификацию под ЭТО → какие строки вообще относятся к вашему упору
4. Поставить эксперимент → своя нагрузка, свои данные, повторы и статистика
5. Посчитать в правильных единицах → на стоимость, на ватт, на стойку, по перцентилям
Пункты 3–5 без пунктов 1–2 дают ровно тот разговор, где сравнивают гигагерцы и количество ядер.
Шаг первый: характеризовать нагрузку, а не железо
В каждый момент нагрузка упирается ровно в один ресурс. Классов узких мест немного, и почти всё практическое умение сводится к тому, чтобы отличать их друг от друга.
| Класс | Что реально ограничивает | Признак в измерении | Что помогает в железе | Что чинится в коде |
|---|---|---|---|---|
| Счёт | заняты исполнительные блоки | высокий IPC, высокая доля полезно завершённых тактов | больше ядер, шире векторы, ускоритель | другой алгоритм, векторизация, меньшая точность |
| Латентность памяти | зависимая цепочка обращений | низкий IPC, промахи последнего уровня, мало параллельных промахов | память с меньшей задержкой, ближе узел NUMA | смена структуры данных, непрерывность вместо указателей |
| Пропускная способность памяти | шина насыщена | трафик к DRAM у измеренного потолка, лишние потоки не ускоряют | больше каналов, быстрее память, второй сокет | блокировка по кэшу, сжатие, меньшая разрядность |
| Ветвления и фронтенд | конвейер пуст | высокая доля ошибок предсказания или простой фронтенда | практически ничего | убрать ветвления по данным, ужать горячий код |
| Ввод-вывод накопителя | ожидание диска | ядра простаивают, очередь длинная | локальные NVMe вместо сетевого тома | батчинг, асинхронный ввод-вывод, другой формат |
| Сеть | ожидание ответа | ядра простаивают, время в ожидании сокета | ближе разместить, быстрее интерфейс | меньше обращений, конвейеризация |
| Синхронизация | потоки ждут друг друга | время растёт с числом потоков | меньше сокетов, ядра в одном кластере | меньше общего состояния, шардирование |
Обратите внимание на последний столбец: в пяти классах из семи изменение кода дешевле изменения железа, и часто эффективнее на порядок. Это первый фильтр любого разговора о закупке.
Диаграмма — не рейтинг, а подсказка, какие строки спецификации вообще имеет смысл читать. Для левого столбца бессмысленно сравнивать пиковую векторную производительность, для правого нижнего — тайминги памяти.
Как определить свой класс измерением
Отношение операций к байтам, оно же roofline. Возьмите число полезных операций за проход и число байтов, реально перегнанных между DRAM и ядром. Их частное — арифметическая интенсивность. Сравните её с точкой перелома платформы: пиковая скорость счёта, делённая на измеренную пропускную способность памяти. Левее перелома вы ограничены памятью, и покупка ядер не даст ничего. Модель предложена в работе Williams, Waterman, Patterson Roofline: An Insightful Visual Performance Model.
Метод двух ручек. Самый дешёвый и самый недооценённый эксперимент: он отвечает на главный вопрос за полчаса и не требует счётчиков. Понизьте частоту ядра процентов на двадцать и измерьте время. Верните частоту, ухудшите память (понизьте её частоту или раздуйте рабочее множество так, чтобы оно вылезло из кэша) — измерьте снова.
время выросло пропорционально снижению частоты ядра → упор в счёт
время почти не изменилось от частоты ядра → упор в память, ввод-вывод или сеть
время выросло от ухудшения памяти → упор в память
не изменилось ни от того, ни от другого → упор в ожидание: диск, сеть, блокировки
Top-Down. Методика раскладывает каждый такт по четырём корзинам: ограничение фронтендом (нечего исполнять), неверная спекуляция, ограничение бэкендом (есть что исполнять, но нельзя) и полезная работа; дальше каждая корзина раскрывается вглубь. Это самый честный способ понять, где теряются такты на внеочередном ядре, где обычные счётчики простоев приблизительны — почему именно, разобрано в главах конвейер и суперскалярность и внеочередное исполнение. Практика применения — в профилировании CPU.
Шаг второй: читаем спецификацию по строкам
Частота: базовая, максимальная и та, что будет у вас
Базовая частота — обязательство: при заявленных условиях охлаждения все ядра выдержат её неограниченно долго. Максимальная — разрешение: ядро может до неё подняться, если сложатся условия. Условия складываются редко.
Максимум обычно достижим одним-двумя ядрами: когда работают все, устойчивая частота ниже, и разрыв между одноядерной и всеядерной частотой для настольных и серверных ядер поколений примерно 2018–2024 годов — порядок 15–30 процентов, а на многоядерных серверных кристаллах больше. Ограничений минимум три, и срабатывает самое строгое: температура кристалла, мгновенный ток через цепи питания и усреднённая по окну мощность пакета — разбор в главе мощность и пределы. Тип инструкций влияет на частоту: широкие векторные и матричные операции переключают больше вентилей за такт, поэтому на них ядро работает ниже, и векторизация маленького участка изредка замедляет программу целиком. Одинаковые изделия работают по-разному: разброс между экземплярами одной модели — следствие производственного разброса и сортировки, см. главу производство.
Практический вывод: сравнивать машины по максимальной частоте бессмысленно. Осмысленно сравнивать устойчивую всеядерную частоту под вашей нагрузкой, а её не печатают — значит, измеряйте.
Ядра: сколько их и какие они
Разнородные ядра. Кристалл часто содержит ядра двух типов: широкие производительные и компактные энергоэффективные, которых на ту же площадь помещается несколько. Для пакетной нагрузки суммарная пропускная способность растёт; для чувствительной к задержке попадание потока на энергоэффективное ядро означает разовое замедление в разы. Планировщик обычно справляется, но поток с жёстким требованием по времени отклика привязывайте явно.
SMT, она же одновременная многопоточность. Два логических потока делят один набор исполнительных блоков, один L1 и один L2. Модель простая: SMT заполняет дыры, которые оставляет один поток.
| Когда SMT выигрывает | Когда SMT вреден |
|---|---|
| поток часто простаивает на промахах памяти — второй занимает освободившиеся блоки | нагрузка уже насыщает исполнительные блоки: два потока делят одно и то же |
| много независимых коротких задач, важна суммарная пропускная способность | важна задержка отдельного запроса: сосед вносит непредсказуемые паузы |
| рабочие множества обоих потоков помещаются в приватный кэш | рабочие множества конкурируют за один L2 и вытесняют друг друга |
| лицензирование считается по физическим ядрам | лицензирование по логическим потокам при выигрыше в 10–20 процентов |
Порядок выигрыша на смешанной серверной нагрузке — 10–30 процентов суммарной пропускной способности при ухудшении задержки отдельного запроса. Третий аргумент: общие ресурсы между потоками образуют канал по побочным эффектам, поэтому в сценариях жёсткой изоляции SMT отключают. И отдельно: один «vCPU» у большинства платформ виртуализации — это логический поток, а не физическое ядро, поэтому восемь vCPU часто означают четыре ядра и масштабируются соответственно (см. виртуализацию и контейнеры).
Кэши: почему «мегабайты L3» сами по себе ничего не значат
Механика разобрана в главе подсистема памяти; здесь только то, что нужно для чтения спецификации.
Уровни различаются не только объёмом, но и принадлежностью: L1 и обычно L2 приватные, поэтому суммарный объём надо делить на ядра — потоку доступен только свой; L3 общий, и делят его все активные ядра, а на многих платформах ещё ускорители и сетевые устройства. Единственное осмысленное сравнение — с рабочим множеством: если горячие данные потока занимают 200 МБ, разница между 16 и 64 МБ общего кэша не меняет ничего, а если данные занимают 1,5 МБ на поток и потоков шестнадцать — те же 24 МБ решают всё. Считайте долю, а не сумму: полезная величина — мегабайты последнего уровня на активное ядро, и кристалл с большим L3 при вдвое большем числе ядер даёт меньше кэша на поток. Топология влияет сильнее, чем кажется: на больших кристаллах последний уровень разрезан на срезы, латентность зависит от расстояния до нужного среза, а кластерная организация ядер делает часть кэша ближе, часть дальше — из спецификации это не читается.
Как измерить своё рабочее множество, не гадая: постройте график времени на элемент от размера обрабатываемого блока. Ступеньки на нём — границы уровней кэша вашей машины и заодно ответ, где сидят ваши данные.
Память: каналы, скорость, тайминги, ёмкость, NUMA
Пиковая пропускная способность считается арифметически: число каналов, умноженное на разрядность канала и на скорость передачи. Порядок величины для восьмиканальной серверной платформы начала 2020-х — сотни гигабайт в секунду на сокет, для двухканальной настольной — десятки. Реально достижимая на потоковом тесте обычно 60–85 процентов от пика: часть тактов уходит на переключение банков, обновление и протокол.
Число каналов важнее частоты: удвоение каналов почти удваивает полосу, а повышение частоты на ступень даёт единицы процентов; при этом каналы заполняются только правильной установкой модулей, и половина пустых каналов означает половину полосы. Частота и тайминги отвечают за разное: частота — за пропускную способность, тайминги — за латентность, и задержка в наносекундах примерно равна значению CL, делённому на половину скорости передачи; для зависимых обращений решает вторая величина, для потоковых — первая. Ёмкость — это порог, а не шкала: пока данные помещаются, лишние гигабайты не ускоряют ничего, а как только не помещаются, начинается своп или отказ и падение измеряется порядками, поэтому ёмкость планируют по пику.
NUMA стоит отдельно. На многосокетной машине, а на некоторых кристаллах и внутри сокета, память разбита на узлы; доступ к чужому узлу дороже локального в 1,5–2 раза и вдобавок отъедает межсокетную шину. Без привязки потоков и памяти многопоточная программа на двух сокетах регулярно работает медленнее, чем на одном с половиной ядер: ровно тот случай, когда «больше железа» без настройки даёт минус.
Ввод-вывод: линии PCIe и где на самом деле узкое место
Пропускная способность одной линии примерно удваивается с каждым поколением: порядок величины — около 1 ГБ/с в одну сторону для третьего поколения, около 2 для четвёртого, около 4 для пятого. Типичный NVMe получает четыре линии, ускоритель — шестнадцать. Считать надо не только линии, но и то, кто с кем их делит.
Линий у платформы конечное число: два ускорителя по шестнадцать линий, четыре NVMe и сетевая карта на 100 Гбит/с могут вместе не поместиться, и часть устройств получит половинную ширину — в спецификации платы это пишут мелким шрифтом. Поколение определяется самым слабым звеном: устройство пятого поколения в слоте третьего работает по третьему. Для ускорителя копирование часто дороже счёта: шестнадцать линий четвёртого поколения дают десятки гигабайт в секунду, внутренняя память ускорителя — на порядок больше, отсюда правило из главы ускорители: выигрыш считается вместе со стоимостью передачи туда и обратно. Для NVMe узкое место обычно не в линиях: четыре линии четвёртого поколения перекрывают потолок почти любого одиночного накопителя по последовательному чтению, а ограничивают глубина очереди, файловая система и характер обращений.
TDP: это тепловой проект, а не измеренная мощность
TDP — величина, на которую рассчитывают систему охлаждения, чтобы изделие работало в заявленном режиме. Это не потребление и не максимум: кратковременный пик заметно выше, средняя мощность на лёгкой нагрузке заметно ниже, а правила определения TDP у разных производителей разные, поэтому цифры между семействами несравнимы. Для планирования нужна измеренная мощность под вашей нагрузкой — счётчики энергии пакета, показания блока питания или PDU — и понимание, что охлаждение и настройки платформы определяют устойчивый режим не меньше самого кристалла: одинаковые изделия в плотном и просторном корпусе покажут разную устойчивую частоту. Физика этого — в главе мощность и пределы.
Наборы команд и специализированные блоки
Строка «поддерживает такой-то векторный набор» означает ровно одно: инструкции не вызовут исключения. Ускорения она не означает. Чтобы наличие превратилось в производительность, нужно одно из трёх: пересборка с указанием целевой архитектуры, диспетчеризация во время выполнения (несколько версий функции и выбор по опросу возможностей) или библиотека, которая делает это за вас. Если дистрибутив собран под базовый уровень архитектуры ради совместимости, расширения простаивают; пределы автовекторизации — в главе SIMD и векторы. То же с матричными умножителями, аппаратным шифрованием, кодеками и блоками нейросетевого вывода: они дают выигрыш в разы и десятки раз, но только внутри своего узкого класса и только через конкретный программный путь. Вопрос при чтении спецификации — не «есть ли блок», а «пойдёт ли моя задача через него и какой библиотекой».
Шаг третий: почему чужие бенчмарки не отвечают на ваш вопрос
Опубликованные результаты не врут — они отвечают на другой вопрос. Источники расхождения по убыванию влияния:
- Другой компилятор и флаги. Разница между сборкой под базовый уровень архитектуры и сборкой под конкретную микроархитектуру на счётном коде — десятки процентов.
- Другие версии библиотек. В линейной алгебре, сжатии, криптографии и регулярных выражениях производительность определяет библиотека, а не процессор.
- Геометрическое среднее по неродственным тестам. Сводный балл смешивает нагрузки, из которых к вам относится в лучшем случае одна: среднее по «сжатие, шахматы, рендеринг, компиляция» не предсказывает поведение вашего сервиса ни в какую сторону.
- Другое охлаждение и другая платформа. Устойчивая частота под долгой нагрузкой зависит от корпуса, радиатора, температуры в помещении и настроек платы.
- Другой объём данных. Тест с рабочим множеством внутри кэша и ваша задача с десятками гигабайт — разные классы нагрузки, а не «то же самое, но больше».
- Другое ядро ОС и другой набор смягчений уязвимостей. Накладные расходы защиты от спекулятивных атак для системно-нагруженного кода измеряются процентами и десятками процентов (см. предсказание переходов).
Как выглядит честный эксперимент
Что это значит практически. Прогрев: первые прогоны измеряют не программу, а холодные кэши, ленивую загрузку страниц, JIT-компиляцию и раскрутку файлового кэша, поэтому их выбрасывают. Повторы и разброс: один прогон — не измерение, нужны десятки, а отчитываться надо медианой и перцентилями, потому что распределение времени почти всегда несимметрично с длинным правым хвостом. Фиксация частоты: динамическое управление частотой — главный источник невоспроизводимости, поэтому для сравнения вариантов кода частоту фиксируют, а для оценки реальной производительности, наоборот, оставляют боевую и измеряют дольше. Изоляция: привязка к конкретным ядрам, вынос прочих задач, отключение SMT на время эксперимента, остановка фоновых задач системы. Одинаковые данные: не синтетика, а срез реального ввода вместе с его перекосом — распределение размеров, доля попаданий в кэш приложения, доля тяжёлых запросов.
Методика целиком — в треке производительности: измерения и бенчмаркинг; как встроить эксперимент в цикл работы — в рабочем процессе оптимизации.
Шаг четвёртый: метрики, в которых считать
Абсолютная производительность почти никогда не является критерием выбора. Критерии — отношения.
| Метрика | Когда решающая | Подвох |
|---|---|---|
| Производительность на единицу стоимости | закупка и аренда под известный объём работы | стоимость владения включает питание, охлаждение, место, обслуживание и людей |
| Производительность на ватт | плотные стойки, батарейные устройства, лимит подводимой мощности | зависит от рабочей точки: то же изделие на пониженной частоте заметно эффективнее |
| Производительность на юнит стойки | дата-центр с ограничением по площади или киловаттам | ограничение чаще по мощности на стойку, чем по месту |
| Задержка в перцентилях | всё интерактивное | среднее скрывает хвост, p99 отличается от медианы в разы |
| Пропускная способность при заданной задержке | сервисы с целевым SLO | максимум пропускной способности достигается при уже неприемлемой задержке |
Разница между «сколько успеет за секунду» и «сколько ждёт один запрос» — центральная. Одна и та же система на 50 и на 90 процентах загрузки даёт почти одинаковую пропускную способность и радикально разную задержку хвоста: очередь растёт нелинейно по мере приближения к насыщению. Поэтому конфигурацию выбирают не по точке максимальной пропускной способности, а по точке, где задержка ещё укладывается в цель. Планирование с этой стороны разобрано в ёмкости, инструменты наблюдения — в наблюдаемости и производительности.
Практика: инвентаризация и измерение на Linux
Первое, что делают с любой машиной, — выясняют, что это вообще такое. Никаких предположений.
## Модель, число ядер и потоков, узлы NUMA, поддерживаемые расширения набора команд
lscpu
## Размеры и уровни кэшей так, как их сообщает ядро; смотреть на строку последнего уровня
lscpu --caches
## Полная топология одной картинкой: сокеты, узлы NUMA, кэши, ядра, потоки,
## привязка устройств PCIe к узлам. Пакет hwloc
lstopo-no-graphics --of console
## Узлы NUMA, объём памяти на узле, матрица относительных расстояний между узлами
numactl --hardware
## Сколько модулей памяти, в каких слотах и какой скорости.
## Половина пустых каналов, то есть половина полосы, видна именно здесь
sudo dmidecode -t memory | grep -E 'Locator|Size|Speed|Rank'
## Устройства и подключение: заявленные и фактические ширина и поколение линий
lspci -vv | grep -E 'LnkCap|LnkSta'
## Режим управления частотой, доступные ступени, включён ли turbo
cpupower frequency-info
Дальше измерение: сначала общая картина по счётчикам, потом раскладка тактов, потом микробенчмарки подсистем.
## Общая картина. IPC около 2 и выше — счётная нагрузка, 0,3–0,5 — ожидание памяти
perf stat -e cycles,instructions,cache-references,cache-misses,branch-misses ./prog
## Раскладка тактов по четырём корзинам Top-Down: Retiring — полезная работа,
## Backend Bound — данные и ресурсы, Frontend Bound — подача команд, Bad Speculation — переходы
perf stat -M TopdownL1 ./prog
## Куда уходит время внутри программы, с разбивкой по функциям
perf record -g ./prog && perf report --stdio
## Пропускная способность памяти потоковым тестом в духе STREAM.
## Сравнивать надо не с паспортным пиком, а вот с этим числом
./stream
## Подсистемы по отдельности
sysbench cpu --cpu-max-prime=20000 --threads=8 run
sysbench memory --memory-block-size=1M --memory-total-size=64G --threads=8 run
И отдельно — режим повторяемости, без которого измерения гуляют на десятки процентов.
## Отключить turbo для повторяемых сравнений вариантов кода; путь зависит от драйвера частоты
echo 1 | sudo tee /sys/devices/system/cpu/intel_pstate/no_turbo
sudo cpupower frequency-set --governor performance
## Привязать процесс к конкретным ядрам, а память — к их узлу NUMA
taskset -c 2,3 numactl --membind=0 ./prog
## Политика реального времени, чтобы фоновые задачи не мешали.
## Осторожно: зависший процесс с такой политикой способен подвесить систему
sudo chrt --fifo 50 ./prog
## Изоляция ядер от планировщика задаётся при загрузке: параметры isolcpus и nohz_full
cat /proc/cmdline
Как читать результат в двух словах: высокий Retiring — нагрузка счётная, помогут ядра и векторы; высокий Backend Bound вместе с промахами последнего уровня — нагрузка на память, помогут каналы, кэш и локальность; высокий Frontend Bound — раздутый горячий код; высокий Bad Speculation — непредсказуемые ветвления, и железо тут не поможет.
Калькулятор выбора: где будет узкое место и что выгоднее
Модель ниже по параметрам нагрузки и машин оценивает достижимую скорость, определяет узкое место и считает стоимость единицы работы. Числа иллюстративные; ценность в структуре расчёта.
"""Узкое место и стоимость работы: roofline плюс закон Амдала.
Модель грубая: она не знает про очереди контроллера памяти, когерентность и
ввод-вывод. Её задача — за минуту показать, в какую стену упрётся нагрузка на
каждой конфигурации и какая конфигурация дешевле на единицу работы.
Сложность: O(M * log C) по времени, где M — число машин, C — число ядер
(перебираются степени двойки), и O(M) по памяти. Считается мгновенно, поэтому
модель удобно гонять по сетке параметров, а не по одному варианту.
"""
from dataclasses import dataclass
CACHE_SPEEDUP = 5.0 # во сколько раз кэш «дешевле» DRAM по эффективной полосе
@dataclass(frozen=True)
class Workload:
name: str
ops_total: float # условных операций на единицу работы
intensity: float # операций на байт трафика к DRAM
working_set: float # байт горячих данных на один поток
parallel_fraction: float # доля работы, которая распараллеливается
@dataclass(frozen=True)
class Machine:
name: str
cores: int
ops_per_core: float # пиковых операций в секунду на ядро
mem_bw: float # байт в секунду, ИЗМЕРЕННАЯ, а не паспортная
llc: float # байт общего кэша последнего уровня
price_per_hour: float # полная стоимость владения, условных единиц
watts: float # измеренное потребление под нагрузкой
def attainable(w: Workload, m: Machine, threads: int) -> tuple[float, str]:
"""Достижимая скорость счёта и имя узкого места при заданном числе потоков."""
compute_roof = m.ops_per_core * threads
# если рабочие множества всех активных потоков влезают в общий кэш,
# трафик к DRAM резко падает, а эффективная полоса растёт
fits = w.working_set * threads <= m.llc
memory_roof = m.mem_bw * (CACHE_SPEEDUP if fits else 1.0) * w.intensity
if compute_roof <= memory_roof:
return compute_roof, "счёт"
return memory_roof, "память в кэше" if fits else "память в DRAM"
def amdahl(fraction: float, threads: int) -> float:
"""Во сколько раз ускорится работа при данной доле параллельной части."""
return 1.0 / ((1.0 - fraction) + fraction / threads)
def evaluate(w: Workload, m: Machine) -> dict:
"""Перебор степеней двойки по числу потоков; лучший режим по стоимости."""
best, threads = None, 1
while threads <= m.cores:
rate, limit = attainable(w, m, threads)
# скорость ограничена и потолком платформы, и последовательной частью
scaled = min(rate, m.ops_per_core * amdahl(w.parallel_fraction, threads))
seconds = w.ops_total / scaled
cand = {"машина": m.name, "потоков": threads, "узкое место": limit,
"секунд": round(seconds, 3),
"стоимость": round(seconds * m.price_per_hour / 3600.0, 6),
"ватт-часов": round(seconds * m.watts / 3600.0, 4)}
if best is None or cand["стоимость"] < best["стоимость"]:
best = cand
threads *= 2
return best
LOADS = [Workload("потоковая агрегация", 2.0e12, 0.5, 512e6, 0.97),
Workload("плотная линейная алгебра", 2.0e12, 24.0, 6e6, 0.99)]
FLEET = [Machine("широкий узел, много ядер", 64, 3.0e10, 180e9, 128e6, 4.0, 400),
Machine("узкий узел, быстрая память", 16, 3.6e10, 140e9, 64e6, 1.6, 180),
Machine("два узких узла рядом", 32, 3.6e10, 280e9, 128e6, 3.2, 360)]
for load in LOADS:
print(f"=== {load.name} ===")
for machine in FLEET:
print(" ", evaluate(load, machine))
Чему учит структура расчёта, независимо от подставленных чисел. Для нагрузки с низкой интенсивностью узкое место — память на всех конфигурациях, и выигрывает та, где больше измеренной полосы на единицу стоимости, а не та, где больше ядер: ядра сверх точки насыщения полосы бесплатны по производительности, но не по счёту. Для нагрузки с высокой интенсивностью и малым рабочим множеством узкое место — счёт, и ядра работают, пока не упрутся в последовательную часть: при доле параллельной части 0,97 потолок ускорения — примерно 33 раза, сколько ядер ни добавляй. А замена одной большой машины двумя меньшими часто выигрывает по полосе памяти и проигрывает по задержке и цене синхронизации, если нагрузке нужно общее состояние (см. многоядерность). Модель полезна ровно до момента, когда вы поставите настоящий эксперимент; её задача — сократить список конфигураций, которые вообще стоит проверять.
Дерево решения: что менять, когда упёрлись
Симптом, железо и более дешёвая починка в коде
| Симптом в профиле | Что менять в железе | Что почти всегда дешевле сделать в коде |
|---|---|---|
| Низкий IPC, промахи последнего уровня, мало параллельных промахов | память с меньшей задержкой, привязка к локальному узлу NUMA | непрерывный массив вместо графа указателей, укрупнить элементы |
| Трафик к DRAM на уровне измеренного потолка | больше каналов памяти, второй сокет, другая платформа | блокировка по кэшу, обработка на месте, меньшая разрядность чисел |
| Высокий Retiring, ядра загружены полностью | больше ядер, шире векторы, ускоритель | другой алгоритм, векторизация, снижение точности вычислений |
| Высокий Frontend Bound | практически ничего | ужать горячий код, оптимизация раскладки по профилю, меньше инлайна |
| Высокий Bad Speculation | практически ничего | убрать ветвления по данным, отсортировать вход, перейти к маскам |
| Время растёт с числом потоков | меньше сокетов, ядра в одном кластере | убрать ложное разделение, шардировать состояние, реже брать блокировки |
| Ядра простаивают, очередь к диску длинная | локальные NVMe вместо сетевого тома | батчинг, асинхронный ввод-вывод, колоночный формат |
| Ядра простаивают, время в ожидании сети | ближе разместить, быстрее интерфейс | объединить обращения, конвейеризовать, кэшировать у клиента |
| Устойчивая частота ниже ожидаемой под долгой нагрузкой | охлаждение, корпус, лимиты платформы | размазать нагрузку во времени, снизить пиковую векторную ширину |
Облако против своего железа: по каким параметрам сравнивать честно
Сравнение «цена инстанса против цены сервера» неинформативно: это разные товары. Честное сравнение делается по списку различий, каждое из которых меняет либо стоимость, либо достижимую производительность.
| Параметр | Арендованный виртуальный ресурс | Физический сервер |
|---|---|---|
| Что такое одна вычислительная единица | обычно логический поток SMT | физическое ядро целиком |
| Изменчивость производительности | соседи делят последний уровень кэша, полосу памяти и ввод-вывод | предсказуемо, вся машина ваша |
| Мелкие конфигурации | часто с накоплением кредитов ЦП: после исчерпания скорость режется в разы | таких режимов нет |
| Дисковая подсистема | сетевой том: задержка на порядок выше локального NVMe, есть лимит операций | локальные NVMe с полной задержкой и полосой |
| Исходящий трафик | тарифицируется отдельно и на объёме становится крупной статьёй | оплачивается канал, а не байты |
| Скорость изменения ёмкости и цена простоя | минуты; платите за включённое | недели и месяцы; платите всегда |
| Обслуживание, замена дисков, гарантия | входит в цену | ваша задача и ваши люди |
Отсюда правила без идеологии. Нестабильная и растущая нагрузка, короткие пики, ранняя стадия продукта — аренда почти всегда выигрывает, потому что вы платите за неопределённость, а не за железо. Стабильная круглосуточная нагрузка известного объёма, живущая годами, — физическое железо начинает выигрывать, и разрыв тем больше, чем выше утилизация; точка безубыточности считается по полной стоимости владения, включая людей и место. Нагрузка, чувствительная к хвосту задержек, — «шумный сосед» бьёт именно по p99, и на выделенном железе разброс существенно ниже, что часто важнее средней цены. Много исходящего трафика — считайте его отдельной строкой до, а не после переезда. Случайный доступ к диску — сетевой том и локальный NVMe различаются по задержке на порядок, и это архитектурное различие, а не настройка. Подробный разбор — в статьях VPS, VDS и выделенные серверы и стоимость облака и компромиссы.
Встраиваемые и батарейные устройства: другая целевая функция
Здесь метрика выбора меняется полностью: пиковая производительность почти не важна.
Энергия на операцию. Устройство просыпается, делает работу и засыпает, поэтому считать надо джоули на завершённую операцию, а не ватты: быстрое ядро, которое успевает досчитать и уснуть, регулярно экономичнее медленного, которое считает вдвое дольше на вдвое меньшей мощности. Ток покоя. Для устройства, которое 99 процентов времени спит, режимы сна и токи утечки важнее всей остальной спецификации вместе взятой: разница между микроамперами и десятками микроампер в глубоком сне — это разница между годом и месяцем работы от одного элемента. Детерминированность. Для управления двигателем или радиотрактом важна гарантированная верхняя граница времени реакции, а не средняя скорость, поэтому здесь сознательно выбирают неглубокий конвейер, отсутствие внеочередного исполнения и память с предсказуемым временем доступа, а кэш иногда отключают ради устранения разброса. Периферия и интеграция. Часто выбор определяет не ядро, а наличие нужных интерфейсов, аналоговых блоков и радиочасти на одном кристалле: это экономит плату, мощность и надёжность.
Арифметика этого разобрана в энергии и ограничениях встраиваемых систем, физическая сторона — в главе мощность и пределы.
Чек-лист выбора железа
- Сформулирована ли цель числом: пропускная способность при заданной задержке, срок расчёта, стоимость единицы работы, время автономной работы.
- Определён ли класс узкого места измерением, а не рассуждением.
- Проверено ли, что узкое место не лечится в коде дешевле, чем покупкой.
- Известно ли рабочее множество в байтах и как оно соотносится с уровнями кэша.
- Измерена ли реальная пропускная способность памяти, а не взята паспортная, и посчитана ли арифметическая интенсивность относительно точки перелома.
- Учтены ли каналы памяти и физическая раскладка модулей, а не только суммарный объём.
- Проверено ли, хватает ли линий PCIe всем устройствам одновременно и в нужном поколении.
- Понятно ли, что даёт SMT именно вашей нагрузке, и по чему считается лицензирование.
- Проверена ли устойчивая частота под долгой нагрузкой в целевом корпусе и охлаждении.
- Измерено ли реальное потребление вместо того, чтобы взять TDP.
- Собрана ли программа под целевую архитектуру и используются ли доступные расширения.
- Есть ли прогон вашей нагрузки на вашей выборке данных — с прогревом, повторами и разбросом.
- Считаются ли задержки в перцентилях, а не в среднем, и посчитана ли полная стоимость владения: питание, место, трафик, обслуживание, люди.
Типичные заблуждения
- «Больше гигагерц — быстрее». Частота — один из трёх множителей времени исполнения (см. обзор трека), и она динамическая. Для нагрузки, упирающейся в память, её повышение не даёт почти ничего; для счётной даёт, но покупается непропорциональным ростом мощности.
- «Больше ядер — быстрее». Прирост ограничен долей параллельной части, полосой памяти и стоимостью синхронизации. Есть нагрузки, которые на большем числе ядер работают медленнее — из-за конкуренции за общий кэш и полосу и из-за роста цены согласования.
- «Возьмём побольше кэша». Кэш даёт эффект скачком, когда рабочее множество в него влезает, и почти не даёт, когда не влезает. Сравнивать надо не мегабайты, а мегабайты на активный поток против размера ваших горячих данных.
- «Купим ускоритель». Ускоритель окупается, если работы много, она однотипная и стоимость передачи данных меньше выигрыша от счёта. Нарушение любого условия делает универсальное ядро быстрее (ускорители).
- «Облако дешевле» и «облако дороже». Обе фразы бессодержательны без утилизации, профиля нагрузки, объёма трафика и стоимости людей: один и тот же сервис при 15 и при 80 процентах утилизации даёт противоположные ответы.
- «В спецификации написана производительность». Написаны пределы. Достижимая доля от предела — отдельная величина, и определяет её код.
- «Синтетический тест показал прирост — значит будет прирост». Он показал прирост на своей арифметической интенсивности, своём рабочем множестве и своей библиотеке. Совпадение с вашей нагрузкой надо доказывать, а не предполагать.
- «У изделий одной модели одинаковая производительность». Разброс между экземплярами, корпусами и версиями микрокода реален и измеряется процентами, а иногда десятками процентов.
Мини-итог
- Выбор начинается не со спецификации, а с классификации нагрузки по узкому месту; классов семь, и в пяти из них код чинится дешевле железа. Roofline отвечает на главный вопрос: левее точки перелома платформы бесполезно покупать счёт, правее — бесполезно покупать память.
- Каждая строка спецификации описывает предел при идеальных условиях: частота — при одном активном ядре, полоса — при идеальном потоке, TDP — тепловой проект, набор команд — лишь отсутствие исключения при исполнении.
- Чужие бенчмарки отвечают на чужой вопрос. Силу имеет одно измерение: ваша нагрузка на ваших данных, с прогревом, повторами и статистикой.
- Решают отношения, а не абсолютные числа: на стоимость, на ватт, на стойку, и задержка в перцентилях, а не в среднем. Облако и своё железо — разные товары; сравнение делается по списку различий и по полной стоимости владения при вашей реальной утилизации.
- Для батарейных устройств целевая функция другая: джоули на операцию, ток покоя и гарантированная верхняя граница времени реакции.
Источники
- Williams S., Waterman A., Patterson D. «Roofline: An Insightful Visual Performance Model for Multicore Architectures» — https://dl.acm.org/doi/10.1145/1498765.1498785: первоисточник модели, вокруг которой построена первая половина главы.
- Hennessy J., Patterson D. Computer Architecture: A Quantitative Approach — глава 1 про то, как честно измерять и сравнивать, и про ловушки сводных показателей.
- Yasin A. «A Top-Down Method for Performance Analysis and Counters Architecture», ISPASS 2014 — https://doi.org/10.1109/ISPASS.2014.6844459: методика Top-Down в первоисточнике.
- McCalpin J. STREAM — https://www.cs.virginia.edu/stream/: эталонный способ измерить реальную полосу памяти. Документация
perf— https://perfwiki.github.io/main/; топология и привязка через hwloc — https://www.open-mpi.org/projects/hwloc/. - Fog A. Optimization manuals — https://www.agner.org/optimize/ и измеренные латентности инструкций — https://uops.info/: чем реальные характеристики отличаются от документации. Gregg B. Systems Performance — https://www.brendangregg.com/systems-performance-2nd-edition-book.html: наблюдение за системой целиком, включая ввод-вывод и сеть.
- Dean J., Barroso L. A. «The Tail at Scale», CACM 2013 — https://cacm.acm.org/research/the-tail-at-scale/: почему перцентили важнее среднего и откуда берётся хвост задержек.
Что дальше
На этом трек закончен. Мы начали с трёх смыслов слова «архитектура» и уравнения времени исполнения, прошли путь от транзистора и вентиля к системе команд как контракту, разобрали, как конвейер, суперскалярность, предсказание переходов и иерархия памяти превращают контракт в производительность, увидели, где заканчивается универсальное ядро и начинаются векторы, многоядерность и специализированные устройства, и уперлись в физику: бюджет ватт, тепло, площадь кристалла и выход годных. Эта глава замкнула круг — те же самые механизмы вернулись в виде строк спецификации, которые теперь читаются не как реклама, а как список пределов с понятным происхождением. Главный навык, который трек пытался передать, — привычка задавать вопрос «чем это ограничено физически» раньше вопроса «что тут можно подкрутить».
Куда идти дальше, зависит от того, что вы делаете с этим знанием:
- Производительность — трек про то, как измерять и чинить: профилирование, бенчмаркинг, работа с памятью и локальностью, встроенный в рабочий процесс цикл оптимизации. Естественное продолжение для тех, кто пишет код.
- Встраиваемые системы: энергия и ограничения — та же физика с другой стороны, где считают микроамперы и джоули, а не гигагерцы.
- Ёмкость и планирование — как превратить понимание железа в решения об инфраструктуре, запасе и стоимости.
- Иерархия памяти и конкурентность и параллелизм — если после глав 09 и 11 хочется закрепить основания.
И наконец, Карта портала — общий обзор треков и маршрутов, чтобы выбрать следующее направление осознанно.