TheCacheWorks
52 subscribers
62 links
Power in knowledge
Download Telegram
​​И этот крокодил тоже Локи - часть 2

Дьявол, как обычно, в деталях.

Без кардинальной переработки ПО проблема блокирования неразрешима. Да, можно ограничиться серверами-маломерками, не более 4х ядер, либо нарезать огромную суповую тарелку на виртуальные машины с ограниченным количеством ядер на машину (что имеет свои проблемы на уровне гипервизора и работы системы в целом, но эти проблемы тоже предпочитают не замечать).

Более того, архитектура thread-safe с ростом числа ядер (тредов) неизбежно упрётся все в того же Амдаля и Уэра. Следовательно, наращивание числа ядер на кристалл имеет весьма мало смысла для типовых задач, так как требует очень специфического программирования.

Итак, очевидно, что короткого и легкого пути не существует. Что насчет других подходов?

Их не очень много.

Семафоры вместо мьютексов (а, точнее, массивы семафоров на общие области памяти с множественным доступом) имеют смысл, однако индустрия предпочитает в этом месте один мьютекс (читай - giant lock) и пусть весь мир подождет. Привет, Оракл. Здравствуй, Бангалор.

Lock-free алгоритмы (включая атомарные операции) таковыми лишь называются. Помимо дьявольской сложности разработки, атомарные операции не являются неблокирующими в полном смысле этого слова. Большинство атомарных операций блокируют шину процессора (пресловутый префикс LOCK, который ставится автоматически - смотрите ассемблер, интрузивные операции всегда блокирующие - в случае сколько-нибудь жестких барьеров памяти), таким образом ждет уже не софт, а хард. Более того, работа с атомарными типами может быть более медленной, чем с обычными данными и, на закуску - совсем не всегда атомарные типы являются под капотом истинными lock-free (да, они могут использовать высокоуровневые мьютексы а не атомарный блокирующий ассемблер). Стоить переборщить с атомиками - и здравствуйте, хардверные ожидания освобождения шины. Нет, число ножек процессора конечно, на каждое ядро свою шину почему-то не сделали. Есть, конечно, каналы, есть NVM. Но сравним число ядер с числом каналов. Добавим кэш третьего уровня, чтобы купировать. Может, четвертый уровень еще? Ну и IO обмажем терабайтными кэшами, чтобы два раза не вставать. То есть, очевидно, что атомарность - не решение.

Подход thread-aware, он же lockless, заключается в снижении уровня блокирования насколько это возможно, в том числе по времени выполнения кода. Заключается он в копировании части общих данных под блоком в TLS (thread-local storage) и последующей работой тредов с локальными копиями без блокирования. Таким образом драматически снижается время удержания блокировок и значительно повышается способность программы к вертикальному масштабированию.

Это хорошее - хотя и неидеальное - решение. На фоне всего прочего. Его недостатком является то, что совсем не всякая задача может быть реализована подобным образом. В реальности требуются серьезные архитектурные проработки прежде, чем нечто подобное может быть эффективно реализовано.

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

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

Performance Team в компании-разработчике ПО - это не роскошь, а насущная необходимость. Столь же серьезная, как и существование департаментов QA.
​​И этот крокодил тоже Локи - часть 3

В финале познакомимся с самым главным Черным Чёртом, который в деталях.

Увидеть Амдаля и Уэра (lock contention) в Линуксе вы влегкую не можете.

Нет в ней в шаговой доступности функциональности lockstat, которая в любой момент под рукой и не просаживает производительность системы, которую нужно мониторить, как в Солярис.

Чтобы увидеть блокировки (и предпринять какие-то действия или хотя бы понять эффект), вам надо для начала собрать ядро с соответствующей опцией.

Из исходников. Задача для гентушника на пару выходных.

И, вдогонку, получить дополнительный немалый оверхед. Оно так написано. И задокументировано. В продуктивной системе это делать никто не станет, само собой разумеется (в Солярис это не так и там в любой момент можно посмотреть, кому стоим - кого ждем; но даже там некоторые админы никогда не слыхали о блокировках и их статистике. Однако для разработки эта система идеально подходит, будучи настоящим инструментальным ящиком).

DTrace и Systemtap? Это для слабаков.

Соответственно, локи это такой зверь, о котором некоторые как бы слышали, но увидеть его не могут - следовательно, он мифический.

Определить lock contention по косвенным признакам?

В добрый час, обычное ожидание блокировки не повышает утилизацию процессоров. Совсем. Вы видите некий hang, при околонулевой утилизации процессоров. Что происходит? Неизвестно. То, что нельзя увидеть - нельзя исправить или хотя бы понять.

Задумавшийся спинлок может увеличивать утилизацию, особенно при дэдлоке. А может и не увеличивать - смотря как он написан. Процесс, застрявший на мьютексе, после ожидания будет сброшен планировщиком в очередь и это тоже впрямую не увидеть. Что-то ждет. Что - непонятно.

Ну, а раз глаз не видит - желудок, соответственно, не страдает.

Проблемы не существует.

Плохие новости - статистика блокировок лишь обозначает проблему. Для ее понимания нужно очень хорошо знать архитектуру ОС и используемого ПО.

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

Разумеется, этого никто не делает и делать не станет. Глупости какие-то, выдумали что-то про блокировки.

Переписывать открытый халявный код? Вот еще, пусть миллион мух напрягается. Им за это даже платят. Может быть. Не мы - мы не платим и не собираемся. Нахаляву уксус сладкий.

Результат немного предсказуем.
​​Долгий джонт

За без малого полстолетия в примитивах синхронизации не придумано ничего лучше семафоров, мьютексов и спинлоков.

Первые два сравнительно просты.

Семафоры это счетчики. Атомарные либо нет. Работают в юзерспейсе, довольно эффективные, и, самое главное, имеют постоянную скорость и оверхед. Кроме, разумеется, ожиданий.

Мьютексы плохи тем, что это всегда syscall (в конечном итоге), переключения контекста, режим ядра со всеми вытекающими. Очень большой оверхед, непредсказуемые ожидания при высокой утилизации. Гибридные реализации, разумеется, существуют (придуманы еще Sun Microsystems), когда сперва мы крутимся на спинлоке, после определенного числа оборотов уходим в мьютекс. Оракл позаимствовал идею для латчей СУБД и имел недокументированный параметр spin_count, позволяющий растянуть время пребывания в режиме спин. Это повышает оверхед, однако снижает лейтенси примитивов синхронизации при выходе из блокировки.

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

Стратегия с уменьшением бэк-оффа достаточно выигрышна в определенных программных реализациях (в частности, в случаях более-менее постоянного времени выполнения критических секций) и демонстрирует отличные результаты в виртуализированных средах.

Если говорить о C++, начиная со стандарта C++20 (наконец-то) реализованы в относительно полном объеме примитивы atomic_flag, позволяющие легковесно реализовывать синхронизацию класса спинлок зачастую без спинлока как такового (с циклом). Оставим за кадром комитет и его техническую политику, а также разработчиков компиляторов.

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

Безусловным плюсом при этом является тот факт, что традиционные спинлоки, содержащие бэк-офф в той или иной реализации (и реализации PAUSE) не позволяют прервать ожидание при изменении состояния флага, совпавшего по времени с ожиданием. Прерывание ожидания технически можно реализовать на conditional variables, однако есть две проблемы - spurious wakeups, которые необходимо обрабатывать - и странные реализации CV в некоторых платформах (привет, FreeBSD).

Атомик флаги решают обе этих проблемы (с той или иной эффективностью на различных платформах), так как wait/notify как раз и позволяют реализовать этот самый переменный бэк-офф с возможностью его прерывания в момент изменения состояния флаговой переменной.

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

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

Второе - высокоскоростной софт зачастую испытывает скачки лейтенси уже от физической реализации DRAM. Те самые RAS/CAS. Эти флуктуации часто вызывают неописуемое изумление, но, увы - физика очень бессердечная штука. Ждем хардвера - означает, ждем хардвера.

Таким образом, мы снова возвращаемся к тому, откуда начали - к необходимости правильного выбора архитектуры.

Без которого сколько-нибудь эффективная реализация масштабируемого (в том числе по вертикали) ПО в принципе невозможна.
​​0,1,2,3,4 - запирайте дверь в квартире

Фредди Крюгер умер за наши грехи.

В пятницу, 13го, приходят плохие новости.

Один из самых опасных багов - Use-after-Free - заметается операционными системами под ковёр.

Все просто - после "освобождения" памяти вызовом free() указатель (обычно shared, и не всегда умный) остается валидным и память автоматически не освобождается.

Нет, Гэри. free() означает буквально следующее - "Сообщаем аллокатору, что эта память может быть освобождена". Программист сам, ручками, должен нуллифицировать указатель после освобождения. Фактическая очистка этой памяти, конечно, может быть выполнена. Вручную. Вызовом безопасного free (который, кстати говоря, стандартом POSIX не определен).

Суть проблемы, однако, в другом. Память кучи контролируется ядром и освобождаемые блоки (мы же помним, что ядра ОС у нас однопоточные и асинхронные? Ведь помним же?) немедленно не очищаются и в пул свободных мгновенно не попадают. Попадают они в очередь освобожденных блоков брокера памяти ядра.

А так как указатель до нуллификации остается валидным, данные в блоке доступны по указателю еще какое-то время.

Вы уже содрогнулись?

Мы содрогнулись, когда увидели, как программисты на Реддите на полном серьезе задавали вопрос - "А нужно ли нуллифицировать указатель после освобождения памяти?"

Держитесь за креслица крепче. Use-after-free крайне трудно обнаружить статическим анализом (анализаторы могут в упор ее не видеть, такие дела), а операционные системы - все без исключения - справедливо считают нуллификацию указателя прерогативой программиста, и все до единой (из общего назначения) имеют очереди брокера памяти ядра. Нет, Раст в такой ситуации не поможет. Анализ потока выполнения между модулями может быть крайне затруднен в случае матерой асинхронщины на коллбэках.

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

По существу, налицо фундаментальная системная проблема безопасности, доказать которую практически невозможно - ну, успехов с таким открытым issue Линуса убедить, что надо убирать очередь освобожденных блоков или кардинально переписывать подсистему памяти.

Еще раз - нуллификация указателя на освобожденную память это прерогатива программиста. Не надо особо рассчитывать на умные указатели либо надеяться на врапперы-обертки.

Вы имеете дело не с опасными ЯП - а, по сути, с опасными ОС. И незнание того, как они работают под капотом, недопустимо для программистов. Вне зависимости от используемых ЯП.
​​Ослик IO и все-все-все, часть 1

Извечная проблема индустрии с некоторых, весьма давних, пор - это непрекращающийся колоссальный IO. Для которого, в общем, существуют весьма близкие лимиты физической пропускной способности. И эти лимиты больно бьют по производительности систем, бюджетам IT-департаментов. А вот производители оборудования - в особенности, сетевых карт и SSD, наоборот, пьют в своих офисах до отвала печени за здоровье айтишников, которые прямо и непосредственно, собственными руками, способствуют повышению стоимости акций этих производителей до небес.

Страдают все - ДБА, системные администраторы, Гретхен.

И лишь вендоры и CTO радуются повышению капитализации и открытию новых датацентров.

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

Проблему IO для понимания следует разделить на две большие части.

Часть первая, архитектурная.

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

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

Про kstat, который без всяких логов собирал и собирает массу статистики без всякого видимого оверхеда - нет, не слыхали.

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

Часть вторая, алгоритмическая.

Всевозможная аналитика и репортинг. Исключая из рассмотрения бизнес-цели необходимости извлечения массы ненужных и неиспользуемых данных (привет, бигдата), стоит отметить технические подходы.

Чрезвычайно неэффективные запросы (привет, квадраты и кубы; здравствуй, ORM с его генерацией SELECT * FROM SELECT * FROM SELECT...) ко всему объему данных, причем неоднократно, в один поток (здравствуй, шардирование - а до него партишенинг; можно было хотя бы параллелить; ах, да - сториджи тоже должны параллельный аксесс аппаратно поддерживать, сюрприз).

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

Нет, разумеется.

Ну а зачем, в самом-то деле? NVMe уже изобрели; что с того, что они как крыло от Боинга стоят?

Никого не волнует эффективность IO. Железо обязано аппаратно справляться с любой мыслимой нагрузкой, алгоритмика это для слабаков.

Оставив в покое алгоритмику - даже такая простейшая и примитивнейшая оптимизация уровня ОС как MAXPHYS это настоящий рокет сайенс и за все время существования использовалась лишь в единичных случаях особо продвинутыми системными администраторами.
​​Ослик IO и все-все-все, часть 2

Исторически с IO сложилась парадоксальная ситуация.

Разработчикам и архитекторам глубоко параллельно, что там происходит под капотом систем, которые они разрабатывают. Бизнесу преподносится идея о том, что все решит оборудование и его нужно как можно больше. Нагрузочное тестирование и оптимизации - это для слабаков. Архитекторы, кроме всего, в детали вникать не собираются, не их это печаль (нет). Преждевременная оптимизация - корень всех зол (беда в том, что оптимизация бывает преждевременной лишь в самом начале - а потом становится уже слишком поздно ее проводить).

Администраторы проблему видят, но мало что могут и хотят сделать - нужно вникать, разбираться, проводить эксперименты - на живых системах, чего никто не позволит; вносить какие-то изменения - страшно; кроме того - "работает? не трогай!"

Когда в конечном итоге приходят проблемы - начинаются лихорадочные поиски волшебной пули.

Суть, однако, в том, что волшебной пули не существует.

Когда система уже построена - без кардинальных или, как минимум, весьма серьезных переделок проблемы IO - о которых никто с самого начала и до данного момента не задумывался - не устранить.

Самое меньшее, что может помочь - переработка алгоритмов и оптимизация систем. Иногда сопряженная с архитектурными изменениями на уровне железа (например, использования, наконец, сбалансированных конфигураций).

Разумеется, никому такие страшно дорогие и очень болезненные операции не нужны.

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

Единственное решение, которое просматривается - с самого начала, от проектирования, иметь Performance Team, следующую мантрам Брендана:

1. Не делай этого.
2. Сделай это только один раз.
3. Делай меньше.
4. Делай позже.
5. Делай это, когда нет других дел.
6. Делай это параллельно.
7. Делай это оптимально.

К IO эти мантры относятся непосредственно.

Сбалансированность конфигураций оборудования с некоторых пор отошла на второй план - гиперконвергентность и горизонтальное масштабирование замели под ковер даже вопросы о сбаланированности.

Однако посейчас наблюдаются случаи (а на десктопах подобные конфигурации вообше норма), когда на 256 ядер в системе приходится лишь один-два SSD.

Старую добрую рекомендацию (для старых и слабеньких процессоров), что на 1 процессор (ядро) должно приходиться минимум пять шпинделей - сейчас уже никто и не помнит.

Резюмируя вышесказанное - проблему IO решить, или, как минимум, значительно ослабить, в общем-то, можно, однако подходить к решению следует системно, от начала проекта и до его завершения. Сама собой эта проблема не решится и хардверными средствами ее решить в полном объеме не получится.
​​Фантастические утечки и где они обитают

Поговорим об утечках памяти.

Утечки памяти делятся на две неравные категории - утечки истинные, подтверждаемые Valgrind и ему подобными инструментами, и утечки мнимые, в действительности утечками не являющиеся.

Первых примерно 40%, в максимуме. Вторых - 60%.

Что такое истинная утечка памяти?

Как всем известно, память кучи выделяется в пространстве памяти ядра ОС и частично контролируется ядром.

Утечка памяти - по определению - это потеря (или нуллификация) всех указателей приложения на блок памяти во время выполнения программы, когда память не была освобождена вызовом free().

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

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

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

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

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

Следует помнить, что мнимые утечки не подтверждены Valgrind и не могут быть им подтверждены, потому что не являются утечками, а, с какой угодно точки зрения, являются легитимными аллокациями.

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

По опыту, на мониторинге истинные утечки обычно выглядят как постоянный небольшой градиент прироста утилизации памяти, с самого начала работы приложения. Медленный и печальный. Утекать может не только память, но и ресурсы, с ней связанные, например, файловые дескрипторы.

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

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

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

Единственная возможность снизить вероятность появления истинных утечек - это тщательное написание кода и тщательное же его тестирование.
​​Почему не прижился memory capping?

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

Когда в 2005 году была предложена (и реализована) идея memory capping, казалось, что вот она, волшебная пуля - разом позволяющая и решить проблемы утечек, и, вместе с остальными ресурсами, ограничить аппетиты систем и приложений по оперативной памяти.

Никто ни на одну секунду не задумался, как написаны приложения и как они будут реагировать на ограничения по оперативной памяти в свете стандартного поведения систем.

Как работает memory capping?

Для приложения (или профиля выполнения или ресурсной группы) задается некое число мегабайт, отслеживаемое операционной системой по заданным условиям. Как только эта величина достигается - операционная система на запросы malloc() от приложения начинает выдавать - нет, не JSON с кодом 403 DENY - а NULL. Это ожидаемое поведение, согласно стандарту.

Что означает для приложения NULL, полученный от аллокатора вместо указателя на выделенную память?

Он означает Out of memory.

Как приложение отреагирует? Как вообще написаны аллокации в приложениях?

Правильно - они написаны очень примитивно: после аллокации приложение - в самом лучшем случае - проверит возвращенный указатель на NULL.

Весьма часто - не проверит. В самом деле, кого это интересует, все равно сделать ничего нельзя, а приложение так и так упадет, fatal.

Если память не может быть выделена - может приложение продолжать работу?

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

Результат немного предсказуем - никто не собирается переписывать ни подсистему памяти ОС с учетом возможности работы какого угодно ресурс менеджмента, ни приложения - от которых ожидается сколько-нибудь корректное поведение с учетом возможности отлупа от менеджера ресурсов.

По этой причине сейчас нигде, ни в каком виде, в ресурс менеджерах ОС невозможно найти memory capping.

В своем непосредственном виде он остался лишь в практически не имеющих применения внутренних менеджерах ресурсов некоторых СУБД.

Вот такой, один из многих, пример, как блестящая с виду идея, не находит, ввиду полной непродуманности, абсолютно никакого вменяемого применения в индустрии.

И единственная, по сути, возможность как-то контролировать расход RAM - это тщательно программировать приложения и настраивать ОС.
​​Тормозяки

Существует один малоизвестный фактор производительности, внезапно и заметно на нее влияющий.

Этот фактор - страничный (файловый) кэш операционных систем.

По умолчанию (а, зачастую, включенный параметром в предположении определенных сценариев работы ОС) он постепенно занимает всю свободную (не занятую ядром и кучей, которая тоже контролируется ядром) память.

Файловые системы используют его для кэширования внешних носителей.

Согласно бодрым заявлениям разработчиков ОС и ФС, эта память может считаться свободной и освобождается незамедлительно, как только будет запрошена.

Так вот, "незамедлительно" это не совсем верно.

Незамедлительно освобождаются лишь те страницы, которые только читались.

Для грязных блоков должен сперва произойти чекпойнт - то есть запись их на файловую систему. А если эти блоки принадлежат активным в данный момент процессам, то и анонимные страницы этих самых процессов должны быть записаны в своп (у вас же есть своп достаточного размера? Ведь есть? Ведь правда?)

Такой, весьма небыстрый - даже для SSD - процесс может быть спровоцирован одной-единственной аллокацией одной-единственной страницы размером 4 кб.

На которой может произойти резкий и внезапный скачок дисковой активности с резким же снижением производительности.

Известно ли об этом разработчикам ОС?

Да, известно.

ZFS, которая славится способностью занимать очень много памяти под кэш, в реализациях натуралов имеет хард лимит кэша ARC (что в принципе мало кому известно), zfs:zfs_arc_max. Данная настройка способна сломать устоявшийся стереотип, что "ZFS сколько памяти находит - столько и занимает". Нет, не занимает. Мы запрещаем пересекать хард лимит. Он потому и хард, что его невозможно перепрыгнуть (хотя были случаи, связанные с критическими багами). Разумеется, нужно устанавливать его в адекватный размер (обычно 1/4 всей RAM).

Всеми нежно любимый за халявность Линукс такого хард лимита на размеры файлового кэша не имеет. Что иногда (внезапно и непредсказуемо) приводит к подобным неожиданным эффектам, как возникающие как бы из ниоткуда тормоза.

В сочетании с тенденцией с установкой огромного количества плашек в сервера и непобедимой тягой девопсов к отключениям свопа это прямо приводит к проблемам, даже на SSD.

Единственная, по сути, возможность - если не избежать, то хотя бы немного сгладить эту проблему - это создавать разделы (не файлы!) подкачки адекватного размера и - для Линукс - воспользоваться параметром vm.min_free_kbytes, задав его пропорционально размеру установленной физической памяти (обычно в размере 5-10% от объема RAM).

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

При этом следует быть очень аккуратным с параметром vm.vfs_cache_pressure, который достаточно сильно влияет на поведение файлового кэша. В сомнительных случаях следует оставить его в умолчании.

Обратите внимание, что рассмотренный фактор способен повлиять как на поведение продуктивных систем, так и на различные бенчмарки (вызывая необъяснимые скачкообразные изменения результатов, о чем будет отдельный разговор).

Резюмируя вышесказанное, оптимизация производительности это кропотливая работа с полным пониманием того, что происходит под капотом в системах. Волшебной пули не существует.
​​Месть Бешеных Чиполлино, часть 1

В чем подвох бенчмарков и почему вменяемые инженеры их глубоко презирают?

Подвохов на самом деле много.

1. Бенчмарки, несмотря на все потуги быть независимыми и тестировать всё и объективно, бывают такими крайне редко. Мало кто из вендоров заинтересован выпячивать свои слабые стороны и топить сильные, поэтому вендорским бенчмаркам веры быть вообще не должно. В принципе. Следует вспомнить историю, на основании которой можно утверждать, что коммерческий успех сверхгиганта nVidia состоялся, почти главным образом благодаря ее инвестициям в разработчиков компании Mad Onion, написавшим необычайно привлекательный 3D бенчмарк, с которого и начался по-настоящему значимый взлет продаж видеокарт. Этот случай нельзя назвать коммерческим подкупом в судебном смысле, однако практическая разница между бенчмарком и последующими попытками выполнения реальных игр на рекламируемых видеокартах (в частности, пресловутого Max Pain, движок которого был использован в бенчмарке) впоследствии не афишировалась. Уплочено. Бенчмарки это коммерческий инструмент и не более. Точка.

2. Если мы говорим не о консьюмерских бенчмарках, а об использовании их в индустрии, следует иметь в виду такой самоочевидный (не для всех, к сожалению) фактор, как статистическая значимость. Чтобы избежать эффекта Майкельсона-Морли (когда один-единственный отрицательный результат был принят как окончательный, что является потрясающим факапом современной физики, по мнению инженеров), следует выполнять множество прогонов и, как минимум, усреднять результаты, а также приводить их хотя бы к 90му перцентилю. Причина? Она очевидна - компьютерные системы сложны и склонны к нелинейному поведению по самым различным причинам. При неоднократных прогонах часто обнаруживается значительный разброс результатов, причины которого почти никогда никто не выясняет (о чем чуть ниже).

3. Правило, которое игнорируется повсеместно - для хоть сколько-нибудь объективной картины бенчмарки (как и индустриальные тесты) следует проводить под полным мониторингом. Иначе говоря, надо, как минимум, включать Заббикс на тестируемых системах. В идеале, надо непрерывно снимать исчерпывающую системную статистику по большинству показателей, зачастую добавляя туда значимые для потребителя метрики - такие, например, как фрагментация памяти и т.п. В подавляющем большинстве случаев, однако, это требование не выполняется и в принятии решений полагаются исключительно на показатели самого бенчмарка. Этим часто грешат даже вендоры (имеющие коммерческий интерес впарить свою продукцию, о чем не следует забывать).

4. Референсные бенчмарки (от вендоров и их аффилиатов) проводятся на специально тюнингованных специалистами экстра-класса системах, что весьма далеко от реальных систем потребителей и заказчиков (как, например, всем известные тесты TPC или SPEC). Причина проста - якобы некоммерческие организации тоже любят деньги и вендоры, разумеется, не будут любить плохие результаты. Говоря начистоту, мало кто из вендоров (если только не имеет реального подавляющего преимущества; и даже в этом случае) не захочет показать товар лицом и продать свои решения. Разумеется, они будут давать рекомендации, доступ к внутренней документации, даже своих специалистов. Вкладывать деньги в результаты - прямо или косвенно. Это бизнес, ничего личного.
​​Месть Бешеных Чиполлино, часть 2

Кто виноват - понятно, что с этим делать и как получить сколько-нибудь достоверные результаты, если вы хоть сколько-нибудь в них заинтересованы? (привет отделам тестирования крупных банков)

1. Бенчмарки - особенно некастомизированные - не показатель. Вообще. От слова "совсем". Тестирование следует проводить на реальных задачах, реальной нагрузке, учитывая статистическую значимость в обязательном порядке и под полным, исчерпывающим мониторингом. В хороших случаях у вендора существует руководство по тестированию, которое следует в обязательном порядке читать и вдумчиво ему следовать. Антипримером является всеобщая любовь, например, к тестированию СХД в один поток утилитой dd, копирующей гигабайтный файл с радостным выводом итоговых IOPSов. Разумеется, это не имеет никакого отношения к реальным профилям нагрузки - но кого это волнует?

2. Выполняя тестирование, следует подходить к нему так же тщательно, как к полноценному IT-проекту: разработка технического задания, выполнение серий тестов, сбор и обработка результатов мониторинга, документирование результатов, принятие решения. Разумеется, следует привлекать квалифицированных специалистов (не одних лишь девопсов, разработчиков или эксплуатационников, для которых тестирование вообще лишняя головная боль и неоплачиваемая нагрузка), возможно, консультируясь с вендором.

3. Тестирование следует продумывать от начала и до конца - то есть до понимания его результатов. Системный мониторинг и его данные играют в этом ключевую роль. Если недостаточно собственной квалификации - следует привлекать квалифицированных специалистов извне (с чем могут быть серьезные сложности, учитывая исчезающую редкость специалистов по оптимизации производительности). Всегда приходится тщательно изучать документацию того, что тестируется, а во многих случаях и вникать глубже - вплоть до изучения поведения, архитектуры или исходного кода. Помимо понимания работы того, что, собственно говоря, тестируется - причем в комплексе.

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

Любые другие подходы, типа однократных прогонов и последующих зашакаленых скриншотов в телеграме - тестами не являются по определению. Это профанация самой идеи объективного тестирования.

С соответствующими результатами.
​​Баллада о верткой пуле, часть 1

Параллельное/конкурентное программирование - это сложно. Несмотря на то, что многоядерникам более 15 лет от роду (а, вообще-то говоря, параллельное программирование появилось много раньше, задолго до того, как многие нынешние разработчики научились ходить), до сих пор параллельное программирование считается (и является) пилотажной задачей, недоступной подавляющему большинству программистов.

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

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

Да, но нет. Это не так работает.

Процессор - не реляционная БД. К нему не прицепишь терабайт оперативной памяти для хранения копий данных перед их изменением (UNDO). К нему не прицепишь SSD для хранения REDO-логов. Память в промышленных количествах пересылать с места на место мгновенно нельзя (здравствуй, memcpy и законы физики).

Аппаратная поддержка так и осталась на уровне "мы будем использовать atomic - а что еще мы можем использовать?". Мы будем использовать кэш-линии и POD-типы - а что еще мы можем использовать? Мы будем использовать fallback to lock, если не можем в транзакцию - а что еще мы можем использовать?

Программная эмуляция - вот всё, что нам, по сути, доступно.

Очень местечковые и экспериментальные реализации, которые дальше занятных экспериментов, по сути, так и не пошли. И в продуктивном коде увидеть такое, даже несмотря на обещаные мифические выигрыши (исключая эзотерику типа Хаскеля), нет практически ни единого шанса.

Результаты несколько предсказуемы.

Эффективно утилизировать десятки ядер в честном параллелизме - трудно. Амдаль с Уэром, рыдают, обнявшись. Сериализация на локах сжирает сколько-нибудь эффективное использование более четырех ядер. Разбиение задач на слабосвязанные потоки возможно совсем не для всех задач, и даже не для большинства. Атомарные операции, если копнуть поглубже, вовсе не являются истинно неблокирующими. Да и написание корректных lock-free/wait-free алгоритмов задачка совсем не для современного сеньора, будем честными.

Конечно же, на тех скоростях, на которых работает большинство ПО, даже lock-free избыточен. Оптимизация и производительность в принципе мало кого волнует до такой степени, чтобы загоняться даже в эффективные микроархитектуру (имеется в виду архитектура ПО, конечно же) или синхронизации.

Поэтому имеем то, что имеем - либо забивание многоядерников формально несвязанным выполнением (здравствуй, виртуализация и докер с кубером, и пусть ОС/гипервизор разгребают), либо сомнительное удовольствие имитации MPP с попытками дробления монолитных задач на слабосвязанные куски с тем, чтобы относительно единовременно исполнять их, по возможности, без синхронизации.

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

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

Собственно говоря, транзакции являются несколько ортогональной концепцией применительно к CPU. Несмотря на это, некоторые разработчики (здравствуй, Амос!) упорно называют транзакциями обычные операции с данными в памяти, которые транзакциями, в прямом понимании этого термина, не являются (подобно термину "бизнес-транзакция", примерно с таким же физическим смыслом).

В техническим смысле транзакцией в памяти следует считать операцию модификации данных, выполняемую истинно атомарно и гарантированно завершающуюся, вне зависимости от конкурентности. При использовании блокировок концепция становится очень простой. Мы ставим блок - в том или ином виде (для атомиков шинный, на уровне железа) - и выполняем изменение. Остальные процессы/треды ждут в последовательной очереди. В точности так выполняются транзакции в реляционных БД.

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

Однако программа, работающая в оперативной памяти и с оперативной памятью, реляционной БД не является.

В отличие от реляционных БД, транзакция в памяти должна быть выполнена, завязаны шнурки или нет. Поэтому для транзакций должен предусматриваться fallback на выполнение под обычной блокировкой - в случае, если транзакция откатывается, попытаться взять блок и снова пытаться выполнить операцию, уже под локом. Ставить транзакции в очередь, как в реляционных БД, мы не можем - процесcор не реляционная БД с терабайтами общей памяти. А другого способа разрешать конфликты изменений не придумано. Даже если создать СУБД-подобную архитектуру CPU, memcpy не является нуль-транспортировкой и ограничена физикой и алгоритмикой - обойтись без этой функции не получится.

Fallback to lock не просто чрезвычайно усложняет предположительно быстродействующий и изначально очень простой код, но и резко увеличивает оверхэд с потенциальным непредсказуемым падением латентности.

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

Все, что у разработчика по факту есть из концептуально приемлемого и хорошо опробованнного - это атомики (и помните, что атомарные операции не являются безусловно неблокирующими, вне зависимости от реализаций), барьеры памяти, все виды традиционных блокировок и lockless/lock-free/wait-free алгоритмы. С программной эмуляцией транзакционной памяти, конечно, можно поиграться, однако, чтобы тащить такое в продакшен, нужно быть очень смелым и очень глупым.

Вывод из сказанного следующий. Так как волшебной пули так и не изобрели, необходимо, вдумчиво используя традиционные средства (к примеру, экспоненциальный бэкофф в спинлоках не всегда благо), тщательно подходить к выбору микроархитектуры (в данном контексте - архитектуры программного кода) и к ее реализации. Медленно и тщательно. Как во времена Кернигана и Ричи. Только таким образом можно получить быстродействующий параллельный код.
​​Короли оверхеда

Откройте для себя RUNPATH.

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

Достаточно сложить нужные библиотеки рантаймов в нужное место (не в системные директории), указать этот путь в RUNPATH при сборке и включить эти библиотеки в дистрибутивный пакет (это умеет делать не только любимый CMake, но даже - тадааааам! - autotools). Всё. Вы прекрасны. Вы только что объехали DLL Hell и можете совершенно безнаказанно (в пределах совместимости системных libC/glibC) обновлять ОС хоста, ее отдельные пакеты, что угодно. Без всяких докеров/флатпаков. Никакого влияния на основную систему.

Помимо версионирования библиотек (на уровне имен и на уровне ABI) и совершенно очевидных проверок API как при компиляции, так и в рантайме - для чего надо приложить лишь некий минимум усилий при разработке - существует совершенно замечательная возможность задать пути поиска динамических библиотек для приложения. Как программными средствами, при компиляции и линковке - так и - в продвинутых случаях - пользуясь штатными методами динамического линковщика.

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

Если вы на полном серьезе верите бодрым заявлениям разработчиков, что виртуализация/контейнеризация почти не привносит оверхеда, то вам следует знать, что это самое "почти" составляет до 20% от производительности барметал. Каждый слой абстракций может привносить такой оверхед. Вряд ли кто-то ограничивается одним докером в погоне за модой и стилем.

Вряд ли кто-то, будучи в здравом уме, может считать оверхед от 20 до 80% приемлемой платой за так называемое удобство и, в особенности, снижение когнитивной сложности.

Давайте взглянем на эти цифры с другой стороны. Вы покупаете 1000 серверов для развертывания своей системы. Скажем, по 10000$ за сервер. Это составляет 10 млн долларов.

Если вы снижаете оверхед на 20%, вы экономите на покупке оборудования 2 млн долларов. Если вы снижаете оверхед на 80% (это экстремальный случай, однако вполне возможный - выкидываем докер, кубер, виртуализацию, отключаем ненужные митигации Spectre/Meltdown, без виртуализации они практически бессмысленны, тюним операционную систему) - вы экономите на железе 8 млн долларов. При этом мы не считаем полную TCO, разумеется. И не рассуждаем о сложности администрирования оставшихся 200 серверов, ансибл уже изобрели.

Нужно считать дальше, варианты с облаками итп.? И так всё очевидно, верно?

Безусловно, отсутствие каких бы то ни было стандартов для библиотек и привело к существующей ситуации. Однако стандарты де-факто все же существуют и поддерживаются строителями компиляторов и binutils. И существуют средства разрешить возникшую проблему.

Надо всего лишь о них знать. И слегка напрячь свою когнитивную сложность. Один раз. При разработке.

Оверхед таких порядков не игрушки. И, разумеется, вам никто из евангелистов не сознается, что он существует и имеет именно такие порядки.

И лишь вам решать - вам к умным или к красивым.
TheCacheWorks pinned «​​Короли оверхеда Откройте для себя RUNPATH. Возможно, для кого-то это станет открытием, однако DLL Hell можно в большинстве случаев объехать простыми, можно сказать - простейшими - методами, неоднократно описанными в документации. Достаточно сложить нужные…»
​​Divide et impera или Все дороги ведут в /lib

Как вы думаете, зачем отцы-основатели, на чьих плечах мы лишь перхоть, раскидали библиотеки в системе по разным директориям?

/lib - система
/usr/lib - юзерлэнд и operational environment
/X11/lib - для гуя
/usr/local/lib - для библиотек локально установленного софта

и так далее?

И для чего изобрели LD_LIBRARY_PATH и RUNPATH? А в наиболее продвинутых и на сегодняшний, 2025й, год, системах сделали тонко настраиваемый ld.so? Для чего изобрели версии API в библиотеках? И их версионирование в принципе?

Отцам-основателям уже было известно, чем пахнут спелые фиги - DLL Hell. И они с ним достаточно успешно справлялись, отделяя мух от котлет. Вдумайтесь только - одна переменная среды или один конфиг ld.so - и проблемы не существует. Не нужно никаких наворотов в виде слоев абстракции, не нужно этих соплей докеров-контейнеров-песочниц. Единственный сколько-нибудь разумный довод - высосанная из пальца якобы безопасность, в эпоху Мелтдауна и Спектров довод просто никакой критики не выдерживает. А их митигации, с 40% шлепком по производительности, это как раз тот самый случай, когда лекарство хуже болезни. Как химиотерапия при неоперабельном раке.

Однако вернемся к нашим барашкам.

И все было хорошо, с бородатыми сисадминами в уродливых свитерах с оленями на первоюниксах.

Однако некий мудрила, родом из пекла, не иначе, как по наущению дьявола, подкинул идейку - "А давайте все вообще библиотеки свалим в /lib. Вообще все. Системные, юзерлэндовские, приложений и пакетов. Ненуачо, удобнажы. И приколотим их гвоздями друг к другу и к libC, будем обновлять дистры как единое целое". LD_LIBRARY_PATH? Не, не слыхал. RUNPATH? Не, не слыхал. Да и вообще, к чему эти точки монтирования, свалим всё в один раздел, пусть система приходит вся и сразу (то, что для разделения системы на разделы тоже есть веские причины - не, не слыхал).

И разверзся портал в Ад.

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

У вас есть 2048+ специализированных дистров для врачей-венерологов, водителей-дальнобойщиков, школьников младшего школьного возраста, школьников среднего школьного возраста...

В которых всё приколочено ко всему гвоздями-сотками. Сколько-нибудь серьезные обновления возможны лишь целиком - а вы попробуйте поставить в Дебиан 11 - OpenSSH 9.9 из Дебиан 12, не сломав всю систему к чертовой матери, попробуйте. Все депенденты в одной точке, разгрести эту кучу в стопочки не представляется возможным - что, кто-то из фанбоев любовно собирает сам, из исходников, с пеной у рта вытребываемых у разработчиков, свои сервисы и приложения, раскладывая библиотеки куда нужно и с пониманием, почему в большинстве пакетов префиксом по умолчанию является /usr/local?

В которых сколько-нибудь стабильная эксплуатация сколько-нибудь значимого и стабильного приложения за пределами LTS - как вам LTS в три года, кстати? Вы, наверное, в курсе, что в нормальном, не пластмассовом, мире LTS - это 10+ лет, не так ли? - возможна исключительно в контейнерах/песочницах. Доводы про безопасность контейнеров развеиваются, как дым - вы по факту замораживаете в контейнерах легаси. Что, серьезно? Обновлять целиком контейнер? То, что контейнеры по факту и изоляции-то сколько-нибудь полной не обеспечивают, как те же гипервизоры - ратующим за безопасность как-то не приходит в светлые головы. Не на том хардвере. Аппаратный no_exec_user_stack на попсовых архитектурах как класс отсутствует.

Вот так нежелание вдумчиво читать отцов-основателей, на чьих плечах мы лишь перхоть, и неудержимое желание реформаторства ради реформаторства, без желания сесть и задуматься - "Может быть, для того, что было сделано - были какие-то веские причины?" - и привело к тому, что мы имеем сейчас.
​​Почему самый лучший аллокатор не ускоряет IO?

TL;DR: Потому, что не может.

Очевидное объяснение.

Даже самый отбитый и отмороженный программист (исключая, возможно, сорокапятилетнего вкатуна - таксиста) не будет писать ввод-вывод с аллокациями-деаллокациями на каждый пук (грусти).

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

Соответственно, на фоне затяжного IO, аллокации занимают проценты (в самом плохом случае) времени.

Очевидно же, правда?

С базами данных (реляционными), на 80% состоящих из этого самого IO, картина другая. Там просто один раз аллоцируются разделяемые (shared) буферы функцией mmap. И движок базы потом в этих буферах сам разруливает. Как арены в игростроительстве. Иногда совсем умные люди даже PGA делают. Для сортировок и прочего.

Действительно ли с базами всё так и приватных аллокаций вообще нет?

Не совсем.

Приватные аллокации в БД выполняют процессы базы. И еще - аллокации приватных страниц выполняют курсоры. Открывающиеся и закрывающиеся.

Отсюда этот видимый (и измеримый) эффект ускорения - чего? - SELECT. Обратите внимание - не DML, лишь в некоторой степени SELECT FOR UPDATE, не DDL с DCL. Не аггрегирующие запросы с чудовищными полными сканами и сортировками.

А стеной идущие относительно мелкие - селекты.

И эффект там значимый. В разы. С одновременным снижением утилизации CPU. Примерно этак вдвое.

Вот что имели в виду бородатые ДБА, которые туманно называли это "снижением оверхэда".

Что же касается влияния супераллокатора на CPU bound и, несколько в меньшей степени, Memory bound, то это уже другая история.

Которая будет рассмотрена позднее.

That's all, folks! 😉
TheCacheWorks pinned «​​Почему самый лучший аллокатор не ускоряет IO? TL;DR: Потому, что не может. Очевидное объяснение. Даже самый отбитый и отмороженный программист (исключая, возможно, сорокапятилетнего вкатуна - таксиста) не будет писать ввод-вывод с аллокациями-деаллокациями…»
​​Камень Всех Камней

Самые аллоцирующие задачи - это CPU Bound.

Не обязательно чисто вычислительные - хотя и они тоже. Любая обработка данных, упирающаяся в частотность ядер.

Зачастую, до прочтения кода или сбора статистики (именно malloc/calloc/realloc/free, не mmap/munmap) никто и не задумывается, как много аллокаций в секунду выполняют такие задачи.

До 40% (а иногда и больше) работы в CPU Bound - это именно аллокации в куче.

Чтобы перекрыть такой оверхед, мало аллоцировать быстро. Нужно аллоцировать с постоянной скоростью, что, как все понимают, труднодостижимо. Дополнительным бременем являются системы с оверкоммитом - это просто какая-то болезнь модная, причем чрезвычайно заразная. На запрос malloc() немедленно возвращается указатель без каких-либо гарантий того, что память действительно выделена. Реальная аллокация происходит лишь после прикосновения к этой памяти, причем не просто прикосновения - это должна быть операция записи. Например, инициализация (что, кстати, достаточно глупо само по себе - mmap() с некоторых пор гарантирует аллокацию инициализированной памяти). То есть, нужно выполнять двойную работу, по факту. Ну или получить указатель в никуда, нежданчиком, начав работать с этой, якобы аллоцированной, памятью. Со всеми отсюда вытекающими последствиями.

Таким образом, оверкоммит (включенный по умолчанию) такое же неизбывное зло, как и THP. Ну, если, конечно, вы не считаете нормальным внезапные падения и рестарт приложений или внезапно же начинающийся на ровном месте свопинг (с отключенным свопом, ха-ха. И как мы раньше-то жили, без оверкоммита и с включенными свопами, трудно представить, правда же?).

Возвращаясь к CPU Bound.

Откуда там аллокации-то? По сути - отовсюду. Элементарный printf - это аллокация. Минимум одна. fprintf - аллокация (да, там можно переназначить буфер на стек - если кто не знает; и подавить аллокацию в куче). Крестовый cout - это очень много аллокаций. Чувствуете, чем пахнут логи? Локальный контейнер в функции (любой контейнер, даже array) - аллокация на входе - хорошо, если одна, если он не ресайзится - и деаллокация на выходе. Хорошо, если разработчик знает о методе reserve(). И использует его. Тогда аллокации можно слегка уменьшить. Но кто, положа руку на сердце, станет на это загоняться? Ну и так далее. Не смотрите вглубь - большое количество даже библиотечных функций являются неявно аллоцирующими (неявно - потому что - кто вообще будет читать их исходники?).

Кастомные аллокаторы принципиально не решают проблемы, будучи приложенными точечно. Потому что все остальные процессы рядом тупят - эффект шумных соседей. И - нет, микросервисы кардинально проблемы не решают. Это просто заметание проблемы под ковер. И - нет, серверлесс тоже не решает проблемы, потому что это маркетинговый термин, а код исполняется не на Великом Небесном Сервере, где-то в варпе, а на вполне реальном железе внутри вполне реальной ОС.

Как известно, под ковер никто не заглядывает.

Чтобы как-то значимо решить данную проблему, не переписывая весь код строка за строкой, нужна одна простая вещь.

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

Со всем вышесказанным хорошо знакомы игроделы - и никто более, потому что всем остальным на уровень ниже фич глубоко начхать. Нет ни времени ни желания этим заниматься и даже просто задумываться.

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

Если, конечно, кто-то догадается их спросить - кто виноват и что делать.
​​Memory Bound - все сложно

По определению, Memory Bound - производительность ограничивается скоростью доступа к памяти, а не частотностью процессорных ядер.

В эту категорию могут попадать задачи с высокой пространственной сложностью - а могут и не попадать. Все зависит от характера алгоритма.

Типичный Memory Bound - данные постоянно перемещаются в память и из памяти, при этом процессор находится в состоянии Idle в ожидании этих самых данных.

Как ни странно, вычисления на GPU, казалось бы, ограниченные объемом видеопамяти, Memory Bound не являются. Если посмотреть вглубь, память там выделяется огромными (и, зачастую, shared) кусками, внутри которых и оперируют процессорные ядра. Однако, внешние по отношению к GPU алгоритмы, которые закачивают данные в видеопамять, могут стать Memory Bound - в силу дисбаланса скоростей и по иным причинам.

Важный момент - Memory Bound может стать любая задача в случае внешней или внутренней фрагментации памяти (здравствуй, Линус, твоя поделка по дефолту дьявольски фрагментирует память - но это же ваш собственный выбор, верно?).

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

Частые мелкие или многопоточные аллокации могут являться ограничивающим фактором Memory Bound - в силу конкуренции за блокировки (привет, системные аллокаторы на мьютексах), кэш-промахов из-за плохой локальности, TLB-misses. Пропускная способность шины памяти также может являться ограничивающим фактором - и тут мы во всей красе встречаем такие факторы, как CAS-latency, а также, здравствуй, NUMA - не в том узле лежат данные.

В целом, по статистике, порядка 40% истинных Memory Bound задач зависят от качества аллокатора. И сколько-нибудь качественных среди известных как-то не наблюдается.

Можно ли что-нибудь втереть или вправить или еще что-нибудь?

Можно несколько снизить остроту проблемы, если:

- минимизировать число аллокаций (здравствуй, метод reserve()!)
- бороться с фрагментацией доступными методами (прочь, THP; долой чрезмерное выделение огромных кусков памяти - да, огромные shared сегменты БД фрагментируются изнутри, сюрприз, и это сжирает весь потенциальный выигрыш), фрагментацию нужно учитывать, хотите вы этого или нет
- NUMA уважайте до опасения, может привести к прямо противоположному эффекту

Если все это не так, вас не спасет ни DDR5 ни DDR10.

Резюмируя вышесказанное, как и во все времена, все определяется качеством алгоритмов. Не методика ХХП - а вдумчивое и тщательное кодирование, тщательная настройка и оптимизация операционного окружения, сбалансированное железо - вот ключ к быстрой и эффективной обработке данных.
​​Закон Протекающих Абстракций, часть 1

Всеобщая тенденция заменять шарящих на дешевых почему-то постоянно разбивается о Закон Протекающих Абстракций.

Возьмем, к примеру, комитет по стандартизации - хоть Си, хоть С++.

С одной стороны, идет перманентная движуха по наворачиванию всевозможных высокоуровневых штуковин. Для примера, хотя бы такая база, как reference.

Хотели убежать от указателей, да? Константная ссылка на инициализированный объект? Задумано-то неплохо, безопасность, тодасё. Однако - под капотом все тот же сырой указатель, и тут же сверху куча оберток std::ref, std::data и прочее и прочее. Да, объект обязан быть инициализированным. Потому что доступ через разадресацию указателя происходит. И что дальше? А дальше то, что на практике оберток оказывается недостаточно и в целой куче случаев надо кастовать - снова в указатель. Уродливо, отвратительно, многословно, небезопасно. Крестовые касты, имеющие несовместимый с сишными ABI. Оно того стоило, а?

Другой пример - выравнивание. Откуда растут ноги? Ноги растут от аппаратной архитектуры. Процессор и его внутренние структуры оперируют машинным словом. А оперативная память адресуется побайтно. И тут начинаются дикие пляски с побайтным чтением из памяти, границей слова и прочим. С++17 наворачивает супервыравнивание в аллокациях new. Затем выясняется, что в многоядерниках с доступом к shared data в кэше надо бы выравнивать по размеру кэш-линии L1 процессора (вы правда думаете, что все на свете CPU имеют кэш-линию 64 бита?), в противном случае false sharing и трындец производительности в горячих местах в многопотоке. SIMD стоит отдельно и размахивает огромным красным знаменем.

Блестяще, мало того, что надо загоняться в выравнивание - его весьма часто надо по размеру кэш-линии выполнять. Комитет умнейшие ребята, std::hardware_destructive_interference_size, компилятор вам скажет, какая кэш-линия. Документация утверждает, что это давно реализовано, зелено, суперзелено, есть проверочный макрос... шта? А компиляторщики сказали - "Э, а как мы определим размер кэш-линии камня, если cpuid есть только у x86? И то не у всех?" (знаете, как кроссплатформенно определить размер кэш-линии L1? Если ни процессор, ни компилятор ничего об этом не говорят? Долго и печально, нехилой такой функцией, измеряющей скорости доступа по массиву данных) Но мы отвлеклись.

Выравнивание вообще отдельная печальная песня. Каждый раз, тыкаясь в память по указателю, на низком уровне надо проверять - выравнены ли данные? Это неслабый такой оверхэд. Спросите - на кой? Ну, как минимум, скорость доступа к невыровненным данным хуже. Как максимум, можно вообще словить UB на многих архитектурах.

В полном соответствии с Законом Протекающих Абстракций, комитет принимает assume aligned.

Ну, то есть, программист, как и в случае с кастами, говорит дословно - "Мамой клянусь, данные выровнены! Не генерируй проверку на выравнивание при доступах!".

Ладно, молчим про текущие абстракции, смотрим - реализовано? Да, реализовано, и давно - суперзелено!

Проверяем - GCC 15.1. Проверочного макроса нет. Вот вообще нет. От слова "совсем". Да, есть интринсик - причем ему сто лет в обед. Да, старые компиляторы интринсик поддерживают, понимают, и генерируют более эффективный ассемблер.

И - оппа! - свежие компиляторы мало того, что не содержат якобы реализованной функциональности. Они еще и интринсик молча игнорируют.

Как же так, Карл? Это же С++, ЯВУ, все низкоуровневые вещи должны быть давно попрятаны в подвал, нейроразнообразные должны фичи пилить, а не о железе и ассемблере думать. Компиляторы умные, они давно должны всё сами-сами-сами делать. Префетчи, restricted reference, assume aligned...

Что? Не выходит Каменный Цветок, Данила-мастер?

Не выходит. Либо думать над кодом и смотреть, что генерируется.

Либо застраивать планету датацентрами с тысячами серверов, исполняющих писанину на питоне и Go.