Offload Core для оптимизации cache miss
Sehnsucht (https://t.me/cxx95)
Несмотря на то, что на многих собеседованиях проверяют знания многопоточки, в low latency сферах его использование жестко ограничено - из тех соображений, что от всяких тяжелых операций на hot path нужно избавляться.
Из этих же соображений всем "задачам", в которых надо что-то немедленно делать (например парсить сетевые пакеты), выделяют отдельное ядро, где собственно крутится busy loop на данной задачи. Такой подход уничтожает все "стандартные" юзкейсы про мьютексы и т.д., потому что не надо делить никакие ресурсы.
[Отступление - "задачам" между собой все-таки нужно общаться, и желательно быстро, поэтому почти что единственная структура, которая часто используется - one-to-one / many-to-one / many-to-many lock-free queue. Иногда встречаются более редкие штуки, например ValueExchange<T>. Иногда какой-то флаг или значение может меняться извне (по мануальному вводу от дежурного), тогда его оборачивают в std::atomic<T> и используют memory_order_relaxed. Стараются держать минимальный набор lock-free кода и использовать его годами, потому что его очень сложно писать и верифицировать]
Задачи с низким приоритетом без busy loop, в том числе системные, выполняются всей кучей на одном "бомж"-ядре. Исторически это нулевое ядро, потому что на него завязано множество системных прерываний Linux, а на другие ядра задачи все равно не будут назначаться, систему так настраивают. Поэтому отдельно есть чеки на то что это ядро не перегружено, иначе высокий риск что всё будет работать не как положено, например нельзя будет работать с машиной по SSH без фризов.
У архитектуры "задача с выделенным ядром" есть один неявный плюс - оно в среднем более cache friendly из-за того что там в среднем выполняется один и тот же код и трогается одна и та же память. На популярных сейчас дизайнах у каждого ядра есть собственный L1 кэш (условно 48 KiB кэш данных + 32 KiB кэш инструкций) и собственный L2 кэш (условно 2 MiB), а также разделяемый всеми ядрами L3 кэш (условно 45 MiB).
Однако если задача помимо "основной работы" может делать какие-то мощные "побочные работы", и делает это часто (не раз в N времени) и там какая-то сильная работа с памятью по типу инференса модели (и это никак не вынести), то к сожалению кэш вымывается, и hot path у tick-to-order страдает от cache miss. Возникает задача - надо делать то, что делается, но не уничтожать кэш по "основной работе".
В таком случае есть подход - взять отдельное ядро, назвать его "offload core", завести там задачу разгребания "работ" в busy loop. Множество "работ" выглядит как lock-free очередь из std::function<void()>, и основное ядро туда пихает упомянутую побочную работу завернутую в лямбду, и в spin-lock ждет пока оно исполнится. Никакой дополнительной синхронизации не нужно как раз из-за ожидания выполнения. И весь фокус в том что выполнение использует кэш другого ядра, а кэш основного ядра не трогается и вымывания не происходит, платя за это несколькими доп инструкциями для работы с очередью.
Для цели "получить минимально возможную software задержку" эта схема реально работает. Но юзкейсов не так много, требуется сильно мерить задержки, и иметь "малодисперсный" сетап где подобная экономия действительно важна.