Архитектура big.LITTLE объединяет в одном процессоре два типа ядер: мощные big с высокой частотой и сложным конвейером, и экономичные LITTLE, которые потребляют в разы меньше энергии, но и считают медленнее. Идея выглядит изящно: лёгкие фоновые задачи живут на маленьких ядрах, тяжёлая работа получает большие. На практике же непонимание того, как именно планировщик и железо делят нагрузку, приводит к парадоксам: приложение на восьмиядерном чипе иногда работает хуже, чем на четырёхъядерном, потому что критичный поток угодил на слабое ядро и сидит там, пока мощное простаивает. Разберём, как устроена гетерогенность, чем её измерить и как добиться предсказуемой производительности.

Как устроены кластеры и чем big ядро отличается от LITTLE

Разница между кластерами не сводится к частоте. Большие ядра семейства Cortex X или Cortex A700 имеют внеочередное исполнение с глубоким переупорядочиванием, широкое окно инструкций, большие кэши второго уровня и агрессивный предиктор переходов. Маленькие ядра линейки Cortex A500 исполняют инструкции преимущественно по порядку, имеют узкий конвейер и компактный кэш. В результате на одинаковой частоте большое ядро исполняет в полтора-два с половиной раза больше инструкций за такт в зависимости от характера кода.

Современные чипы нередко добавляют третью ступень. Трёхкластерная схема включает одно мегапроизводительное ядро, два-три средних и четыре экономичных. Планировочные решения усложняются, а код, рассчитанный на простую бинарную модель, начинает размещаться неоптимально. Узнать топологию помогает чтение sysfs: каталоги /sys/devices/system/cpu/cpuN/cpufreq содержат related_cpus и cpuinfo_max_freq, и по группировке ядер с одинаковыми значениями легко восстановить границы кластеров. Поле capacity в /sys/devices/system/cpu/cpuN/cpu_capacity отражает относительную мощность, которую использует планировщик при выборе размещения.

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

Как планировщик Linux видит большие и маленькие ядра

Классическая модель CFS плохо понимает неоднородность, поэтому в ядре появился механизм EAS, Energy Aware Scheduling. Планировщик строит энергетическую модель процессора: каждый кластер описан набором операционных точек частоты с соответствующим энергопотреблением, а каждая задача имеет оценку утилизации. При выборе ядра EAS минимизирует прогнозируемую энергию при условии, что задача поместится в вычислительную мощность кандидата.

Утилизация задачи считается механизмом PELT, Per Entity Load Tracking, который ведёт экспоненциально затухающую среднюю активности. На гетерогенных системах эта величина нормируется через cpu_capacity: задача, полностью занимающая маленькое ядро, получает большую относительную утилизацию и становится кандидатом на переезд в большой кластер. Порог срабатывания определяет, насколько рано потоки будут мигрировать вверх по производительности.

Проблема в инерционности. PELT реагирует на всплеск нагрузки за миллисекунды, а интерактивный поток успевает пропустить кадр, пока планировщик поймёт, что задача тяжёлая. Поэтому в продуктовых ядрах применяется uclamp, механизм, позволяющий задавать минимальные и максимальные границы утилизации через sched_setattr. Повышенное значение uclamp.min подталкивает планировщик размещать поток сразу на большом ядре, не дожидаясь истории нагрузки. Это прямой и честный способ сказать системе, что поток важный, но злоупотребление им превращает энергоэффективный чип в жадный.

Методика измерения производительности каждого кластера

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

  1. Прогон вычислительного теста на самом слабом ядре через taskset с маской одного процессора;
  2. Тот же тест на самом мощном ядре с записью отношения времён для калибровки смешанных моделей;
  3. Прогон многопоточной нагрузки с наблюдением, куда планировщик разместил потоки, через чтение /proc/pid/stat и поля processor;
  4. Повтор всех замеров после десятиминутного прогрева для оценки термального режима.

Для понимания того, что происходит внутри, незаменим perf. Счётчики PMU различаются между микроархитектурами, и perf автоматически подхватывает правильные события для каждого ядра: циклы, исполненные инструкции, промахи кэша, неверно предсказанные переходы. Отношение инструкций на такт на большом и малом ядре наглядно показывает разрыв в эффективности конвейера. Инструменты вроде simpleperf в Android собирают профили с привязкой событий к ядрам и позволяют увидеть, сколько времени поток реально провёл на каждом типе процессора.

Отдельного внимания заслуживает частота. Большое ядро на низкой частоте может проигрывать маленькому на максимальной, если границы диапазонов пересекаются, а механизм DVFS реагирует медленнее, чем мигрирует задача. Мониторинг scaling_cur_freq параллельно с нагрузочным тестом показывает, не ограничивает ли кластер температурный контур. Стабильные измерения получают только после прогрева, убедившись, что термальный потолок не включился посреди прогона.

Типичные патологии распределения и способы лечения

Самая частая беда это застревание тяжёлого потока на маленьком ядре. Причин несколько: агрессивная политика энергосбережения, низкое значение uclamp.min, короткая природа нагрузки, которая не успевает накопить утилизацию PELT. Лечение начинается с явных подсказок планировщику. Через механизм cpuset потоки критичного сервиса привязывают к маске больших ядер, а фоновым задачам отводят маленькие. В мобильных стеках подобную роль выполняют группы планирования, где задачи переднего плана получают преимущество автоматически.

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

Третья проблема тоньше: нагрузка состоит из потоков сильно разного веса, и планировщик размещает их так, что два тяжёлых оказываются попарно на одном большом ядре, а два лёгких гуляют на маленьких. Результат хуже наивного закрепления. Здесь помогает осознанное разделение: тяжёлые потоки собираются в отдельный cpuset, внутри которого ядер не меньше, чем потоков, чтобы исключить деление одного процессора.

Влияние DVFS и термального троттлинга на итоговые цифры

Динамическое управление частотой усложняет картину принципиально: производительность становится функцией времени. Современный губернатор schedutil вычисляет частоту из той же утилизации, что использует EAS, и на бумаге механизмы согласованы. На практике всплеск нагрузки сначала обрабатывается на низкой частоте, затем частота повышается, затем, возможно, включается миграция на большое ядро. Каскад решений занимает миллисекунды, и короткие задачи исполняются целиком на минимальной производительности.

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

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

Отладка решений планировщика через трассировку

Когда распределение ведёт себя непредсказуемо, встроенные механизмы трассировки показывают логику планировщика изнутри. События sched_switch и sched_migrate_task позволяют восстановить полную историю путешествий потока по ядрам, включая момент и причину переезда. По длине цепочки migrate за единицу времени отличают здоровую балансировку от патологического метания.

Запись sched_wakeup показывает, на каком ядре поток просыпался и сколько ждал в очереди до исполнения. Для интерактивных задач критична именно эта задержка пробуждения, и её карта по типам ядер быстро объясняет, почему система кажется заторможенной: если поток систематически просыпается на маленьком ядре и ждёт, пока до него дойдёт очередь, никакая чистка кода интерфейса не спасёт.

Статистика /proc/sched_debug раскрывает настройки доменов планировщика: флаги балансировки, интервалы, границы групп. На гетерогенной системе полезно убедиться, что домен верхнего уровня охватывает оба кластера с флагом asymmetric packing, иначе планировщик может недальновидно заполнять маленький кластер до отказа, пока большие ядра пустуют.

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

Итоговое правило отладки простое: сначала смотрят, что решил планировщик, затем почему он так решил, и только потом спорят с ним через affinity и подсказки. Обратный порядок, когда маски расставляют вслепую, создаёт системы, которые работают быстро только на докладных слайдах и распадаются при первом изменении характера нагрузки.

Практическая настройка продуктовых систем

Продуктовая система на гетерогенном ARM начинается с аудита топологии, записанного в конфигурацию, а не предполагаемого по документации. Версии одного чипа от разных поставщиков различаются частотами кластеров и набором ядер, и жёстко прошитые маски ломаются на соседней модели устройства. Динамическое обнаружение через sysfs делает конфигурацию переносимой.

Дальше определяют роли. Интерактивные и критичные по задержке потоки получают cpuset из больших ядер и повышенный uclamp.min. Фоновые синхронизации, загрузчики и архиваторы садятся на маленькие ядра. Потоки, обслуживающие прерывания периферии, привязываются к ядрам кластера, ближнего к соответствующему контроллеру, чтобы не гонять события через весь чип.

Контроль результата нельзя оставлять на интуицию. Регулярное снятие perf stat по типам ядер, мониторинг миграций и частот в работе, сравнение времени отклика до и после описанных мер превращают гетерогенность из источника сюрпризов в рычаг. Архитектура big.LITTLE перестаёт быть лотереей ровно в тот момент, когда инженер знает, какие потоки где должны жить и почему, а планировщик получает от системы все подсказки, чтобы не передумывать.