Перевод Game of Low Latency


В детстве я был готов подраться из-за критики любого фильма с Брюсом Ли. Теперь, будучи взрослым и более утонченным фанатом, я могу признать, что его последний фильм, “Game of Death”, - отстой. В защиту Ли хочу сказать, что он умер во время съемок, поэтому для спасения фильма продюсерам пришлось пересмотреть основные сюжетные моменты. Но то, что осталось от его первоначального сценария, а именно: главный герой, сталкивающийся в финале со все более грозными противниками на разных уровнях пирамиды, - повлияло на десятки фильмов и игр на десятилетия вперед. Как и игры в стиле Mortal Kombat, вдохновленные его посмертным фильмом, оптимизация low latency - это своя собственная многоуровневая игра. Кто является противниками на каждом уровне в “Игра в Low Latency” и какие задачи ставит каждый из них?

Игра в Low Latency

LATENCY LEVEL 1 – ИТ-индустрия

Ваш первый противник - “ИТ-индустрия”, и он хочет впихнуть вам в голову все, что связано с высокой пропускной способностью(high-throughput). Он заставит вас бороться с настройками по умолчанию, основанными на пропускной способности, во всем - от серверов и сетевых коммутаторов до ОС и приложений с открытым исходным кодом/COTS. Хуже того, ~90% доступных руководств по “лучшим практикам настройки” предполагают высокую пропускную способность в качестве главной цели читателя.

Сколько раз настройки TCP по умолчанию, такие как Nagle или Delayed ACK, портили вам жизнь в поисках низкой задержки? NIC, которые задерживают оповещение вашего приложения о прибытии первого пакета, чтобы успеть забуферизировать дополнительные входящие пакеты? Рекомендации производителей серверов, которые пропагандируют широкое чередование Memory Channel? Инструменты мониторинга, которые сообщают не более чем среднее значение и стандартное отклонение, как будто system latency вообще следует нормальному распределению? Список можно продолжать бесконечно.

LATENCY LEVEL 2 – The Hardware

“The Hardware” видит ваше триумфальное прибытие на второй уровень и ехидно смеется. Он бросает в вас ядовитые System Management Interrupts (SMI), которые приостанавливают работу ОС для запуска встроенного программного обеспечения для различных целей администрирования. Эти SMI лишат ваше приложение от сотен микросекунд до 100 мс времени выполнения.

По мере роста частоты ядра, увеличения числа ядер и повышения плотности компонентов, функции регулирования энергопотребления и тепловыделения становятся все более распространенными. Все, от материнской платы и процессора до устройств PCIe и модулей DDR DIMM, сговорились дремать на работе. Несколько лет назад, например, наш поставщик серверов случайно поставил нам системы с DIMMs configured for Opportunistic Self-Refresh. Таким образом, после коротких периодов затишья DIMM автономно переходили в режим REFRESH, во время которого запросы процессора не обслуживались. Процессоры и устройства PCIe ищут подобные периоды затишья, чтобы погрузиться в более глубокие C-states и ASPM L-states, соответственно. Чтобы отключить эти функции, необходимо пробраться через лабиринт настроек BIOS, ядра и ОС. Мне кажется, что каждый год добавляют новую партию функций управления питанием/тепловыделением, за которыми нужно следить.

Электропитания процессора

LATENCY LEVEL 3 – The Kernel

Вы уверенно поднимаетесь на встречу с “The Kernel” на третий уровень этой прекрасной игры. В отличие от “The Hardware” на уровне 2, этот парень не улыбается - он весь в делах. Он бросает в вас всевозможные дротики для повышения пропускной способности, и с каждым релизом у него появляется все больше боеприпасов. NAPI, automatic NUMA balancing, Transparent Huge Pages (THP) и т.д. Не говоря уже о множестве kernel workqueue threads для всех типов фоновых задач. Все это звучит прекрасно, пока вы не поймете, что они часто препятствуют вашим усилиям по снижению задержки.

Учитывая резко сокращающиеся затраты времени на обработку пакетов при скорости сетевых карт 25/40/100 Гбит/с, сетевой уровень ядра просто не в состоянии удовлетворить ваши потребности в сетях с низкой задержкой. А планировщик CFS в Linux может быть справедливым, но “справедливость” - это анафема в пространстве низких задержек, где ценится приоритетный доступ. И даже не начинайте рассказывать мне обо всех устройствах, таймерах и function call interrupts, которые необходимо минимизировать.

LATENCY LEVEL 4 – The Application

Вы сделали это! Вы добрались до Главного Босса, “The Application”, на четвертом уровне! Если на предыдущих уровнях вы чувствовали себя тревожно, то перед последним врагом ваше владение C++ наполняет вас уверенностью. Вы тщательно выбирали lock-free структуры данных и алгоритмы с учетом Mechanical Sympathy. Ваши показатели попадания в кэш L1d и branch prediction оптимальны. Но мудрый воин никогда не недооценивает своего врага - и, как выяснилось, у этого врага есть неплохой арсенал.

Weapon #1: Address Space

Провели ли вы pre-allocate и pre-fault всей памяти, которая вам понадобится? Используют ли ваши STL контейнеры эту предварительно выделенную память? Если ваше приложение многопоточное, вы также pre-fault стеки потоков? Если не учесть эти моменты, то во время рантайма мелкие ошибки страниц будут стоить ~1 мкс каждая, что является вечностью.

Pre-allocate означает предварительное выделение ресурсов, таких как память, перед их фактическим использованием Это делается для того, чтобы избежать ситуаций, когда приложению потребуется больше ресурсов, чем доступно что может привести к сбоям или снижению производительности Предварительное выделение ресурсов позволяет обеспечить более предсказуемое поведение приложения и улучшить его стабильност.

Pre-fault - это стратегия, используемая для оптимизации использования памяти. Она заключается в предварительной загрузке данных в память, которые, вероятно, потребуются в ближайшее время Это позволяет избежать ситуаций, когда данные загружаются в память только тогда, когда они действительно нужны что может замедлить работу приложения. Pre-fault помогает уменьшить количество обращений к диску или сети для загрузки данных что повышает общую производительность приложения.

Ваш аллокатор освобождает память и возвращает ее в ОС, возможно, после множества вызовов free или delete, вызванных RAII? Если да, и ваше приложение многопоточное, то вы столкнетесь с latency-spiking от Translation Lookaside Buffer (TLB) Shootdowns. Этот вид прерываний плох на принимающих ядрах(receiving cores), но еще хуже на отправляющих ядрах(sending core). И даже неважно, что это ваш фоновый поток, выполняющий все дорогостоящие аллокации/деаллокации. Все эти потоки делят одно и то же адресное пространство, так что в итоге расплачиваются все.

А если у вас page faulting и освобождение памяти обратно в ОС, приготовьтесь столкнуться с трудностями, вызванными борьбой за блокировку mmap_sem. И вот еще интересный факт о блокировке mmap_sem: При чтении procfs memory map files определенного процесса эта блокировка переходит в режим чтения для данного процесса. Можете ли вы представить, что Monitoring Tool impacting the latency of your application, просто сообщая об использовании его памяти? Это грязная игра, Игра в Low Latency.

Weapon #2: CPU Cache

False sharing - еще одна распространенная ловушка задержки в многопоточных приложениях, когда несколько потоков обращаются к разным объектам, находящимся в одной и той же 64-байтной строке кэша. В Universal Scalability Law (USL) это связано с Coherence factor (β > 0), который накладывает отрицательный эффект на производительность. Другими словами, это убийца низкой latency/scalability. Intel VTune Pofiler и perf c2c - вот оружие на такой случай.

В вашем приложении есть редко выполняемые функции, которые, тем не менее, должны быстро срабатывать при обращении к ним? Лучше использовать метод cache-warming, чтобы сохранить соответствующие инструкции и данные в i-cache и d-cache соответственно.

CppCon 2018 Lightning Talk рассказывали об одной технике cache-warming, которая позволяет избежать branch misprediction

Хотя потоки вашего приложения могут быть прикреплены к отдельным ядрам с отдельными L1/L2 кэшами, борьба будет вестись на общем LLC уровне. А в наши дни производители чипов распределяют LLC по a mesh or a network of core complexes. Поэтому задержка доступа будет зависеть от того, насколько далеко ядро находится от данного LLC-фрагмента.

Weapon #3: Compiler Flags

Вы, вероятно, никогда не думали дважды о компиляции своего приложения с -O3, несмотря на то, что исследования показывают постоянное противостояние между производительностью -O2 и -O3 среди компиляторов. А как часто вы страдаете от ~10 мкс периода снижения IPC, когда в вашем коде выполняются даже легкие инструкции AVX2/AVX512? Теперь вы можете подумать: “Но я же не использую в своем коде векторные инструкции?” Ну, если вы компилируете с -march=native, то не будьте так уверены. Посмотрите на сгенерированную сборку или сравните производительность после добавления опции сборки -mprefer-vector-width=128.

Game (Not) Over

Эта статья дает лишь поверхностный обзор, ведь нужно учитывать многое другое. Но, надеюсь, я пролил свет на различия между оптимизацией пропускной способности и оптимизации задержек (throughput vs latency).

Мы никогда не сможем по-настоящему победить в этой многоуровневой игре, как это делает герой Брюса в Game of Death. Обстановка слишком сильно меняется с каждой новой технологией, выпуском нового чипа, обновлением ядра или усовершенствованием языка программирования, чтобы мы могли завершить этот процесс. Но благодаря культуре обмена знаниями, открытого экспериментирования и Active Benchmarking у нас, по крайней мере, будет бесконечное количество жизней, чтобы продолжать играть.

The Game is on!


Комментарии в Telegram-группе!