Тормозяки
Существует один малоизвестный фактор производительности, внезапно и заметно на нее влияющий.
Этот фактор - страничный (файловый) кэш операционных систем.
По умолчанию (а, зачастую, включенный параметром в предположении определенных сценариев работы ОС) он постепенно занимает всю свободную (не занятую ядром и кучей, которая тоже контролируется ядром) память.
Файловые системы используют его для кэширования внешних носителей.
Согласно бодрым заявлениям разработчиков ОС и ФС, эта память может считаться свободной и освобождается незамедлительно, как только будет запрошена.
Так вот, "незамедлительно" это не совсем верно.
Незамедлительно освобождаются лишь те страницы, которые только читались.
Для грязных блоков должен сперва произойти чекпойнт - то есть запись их на файловую систему. А если эти блоки принадлежат активным в данный момент процессам, то и анонимные страницы этих самых процессов должны быть записаны в своп (у вас же есть своп достаточного размера? Ведь есть? Ведь правда?)
Такой, весьма небыстрый - даже для SSD - процесс может быть спровоцирован одной-единственной аллокацией одной-единственной страницы размером 4 кб.
На которой может произойти резкий и внезапный скачок дисковой активности с резким же снижением производительности.
Известно ли об этом разработчикам ОС?
Да, известно.
ZFS, которая славится способностью занимать очень много памяти под кэш, в реализациях натуралов имеет хард лимит кэша ARC (что в принципе мало кому известно),
Всеми нежно любимый за халявность Линукс такого хард лимита на размеры файлового кэша не имеет. Что иногда (внезапно и непредсказуемо) приводит к подобным неожиданным эффектам, как возникающие как бы из ниоткуда тормоза.
В сочетании с тенденцией с установкой огромного количества плашек в сервера и непобедимой тягой девопсов к отключениям свопа это прямо приводит к проблемам, даже на SSD.
Единственная, по сути, возможность - если не избежать, то хотя бы немного сгладить эту проблему - это создавать разделы (не файлы!) подкачки адекватного размера и - для Линукс - воспользоваться параметром
Этот параметр резервирует заданную часть свободной памяти от поползновений страничного кэша и позволяет ядру выкрутиться в ситуации, когда количество грязных страниц в файловом кэше быстро и угрожающе растет.
При этом следует быть очень аккуратным с параметром
Обратите внимание, что рассмотренный фактор способен повлиять как на поведение продуктивных систем, так и на различные бенчмарки (вызывая необъяснимые скачкообразные изменения результатов, о чем будет отдельный разговор).
Резюмируя вышесказанное, оптимизация производительности это кропотливая работа с полным пониманием того, что происходит под капотом в системах. Волшебной пули не существует.
Существует один малоизвестный фактор производительности, внезапно и заметно на нее влияющий.
Этот фактор - страничный (файловый) кэш операционных систем.
По умолчанию (а, зачастую, включенный параметром в предположении определенных сценариев работы ОС) он постепенно занимает всю свободную (не занятую ядром и кучей, которая тоже контролируется ядром) память.
Файловые системы используют его для кэширования внешних носителей.
Согласно бодрым заявлениям разработчиков ОС и ФС, эта память может считаться свободной и освобождается незамедлительно, как только будет запрошена.
Так вот, "незамедлительно" это не совсем верно.
Незамедлительно освобождаются лишь те страницы, которые только читались.
Для грязных блоков должен сперва произойти чекпойнт - то есть запись их на файловую систему. А если эти блоки принадлежат активным в данный момент процессам, то и анонимные страницы этих самых процессов должны быть записаны в своп (у вас же есть своп достаточного размера? Ведь есть? Ведь правда?)
Такой, весьма небыстрый - даже для 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). Причина проста - якобы некоммерческие организации тоже любят деньги и вендоры, разумеется, не будут любить плохие результаты. Говоря начистоту, мало кто из вендоров (если только не имеет реального подавляющего преимущества; и даже в этом случае) не захочет показать товар лицом и продать свои решения. Разумеется, они будут давать рекомендации, доступ к внутренней документации, даже своих специалистов. Вкладывать деньги в результаты - прямо или косвенно. Это бизнес, ничего личного.
В чем подвох бенчмарков и почему вменяемые инженеры их глубоко презирают?
Подвохов на самом деле много.
1. Бенчмарки, несмотря на все потуги быть независимыми и тестировать всё и объективно, бывают такими крайне редко. Мало кто из вендоров заинтересован выпячивать свои слабые стороны и топить сильные, поэтому вендорским бенчмаркам веры быть вообще не должно. В принципе. Следует вспомнить историю, на основании которой можно утверждать, что коммерческий успех сверхгиганта nVidia состоялся, почти главным образом благодаря ее инвестициям в разработчиков компании Mad Onion, написавшим необычайно привлекательный 3D бенчмарк, с которого и начался по-настоящему значимый взлет продаж видеокарт. Этот случай нельзя назвать коммерческим подкупом в судебном смысле, однако практическая разница между бенчмарком и последующими попытками выполнения реальных игр на рекламируемых видеокартах (в частности, пресловутого Max Pain, движок которого был использован в бенчмарке) впоследствии не афишировалась. Уплочено. Бенчмарки это коммерческий инструмент и не более. Точка.
2. Если мы говорим не о консьюмерских бенчмарках, а об использовании их в индустрии, следует иметь в виду такой самоочевидный (не для всех, к сожалению) фактор, как статистическая значимость. Чтобы избежать эффекта Майкельсона-Морли (когда один-единственный отрицательный результат был принят как окончательный, что является потрясающим факапом современной физики, по мнению инженеров), следует выполнять множество прогонов и, как минимум, усреднять результаты, а также приводить их хотя бы к 90му перцентилю. Причина? Она очевидна - компьютерные системы сложны и склонны к нелинейному поведению по самым различным причинам. При неоднократных прогонах часто обнаруживается значительный разброс результатов, причины которого почти никогда никто не выясняет (о чем чуть ниже).
3. Правило, которое игнорируется повсеместно - для хоть сколько-нибудь объективной картины бенчмарки (как и индустриальные тесты) следует проводить под полным мониторингом. Иначе говоря, надо, как минимум, включать Заббикс на тестируемых системах. В идеале, надо непрерывно снимать исчерпывающую системную статистику по большинству показателей, зачастую добавляя туда значимые для потребителя метрики - такие, например, как фрагментация памяти и т.п. В подавляющем большинстве случаев, однако, это требование не выполняется и в принятии решений полагаются исключительно на показатели самого бенчмарка. Этим часто грешат даже вендоры (имеющие коммерческий интерес впарить свою продукцию, о чем не следует забывать).
4. Референсные бенчмарки (от вендоров и их аффилиатов) проводятся на специально тюнингованных специалистами экстра-класса системах, что весьма далеко от реальных систем потребителей и заказчиков (как, например, всем известные тесты TPC или SPEC). Причина проста - якобы некоммерческие организации тоже любят деньги и вендоры, разумеется, не будут любить плохие результаты. Говоря начистоту, мало кто из вендоров (если только не имеет реального подавляющего преимущества; и даже в этом случае) не захочет показать товар лицом и продать свои решения. Разумеется, они будут давать рекомендации, доступ к внутренней документации, даже своих специалистов. Вкладывать деньги в результаты - прямо или косвенно. Это бизнес, ничего личного.
Месть Бешеных Чиполлино, часть 2
Кто виноват - понятно, что с этим делать и как получить сколько-нибудь достоверные результаты, если вы хоть сколько-нибудь в них заинтересованы? (привет отделам тестирования крупных банков)
1. Бенчмарки - особенно некастомизированные - не показатель. Вообще. От слова "совсем". Тестирование следует проводить на реальных задачах, реальной нагрузке, учитывая статистическую значимость в обязательном порядке и под полным, исчерпывающим мониторингом. В хороших случаях у вендора существует руководство по тестированию, которое следует в обязательном порядке читать и вдумчиво ему следовать. Антипримером является всеобщая любовь, например, к тестированию СХД в один поток утилитой dd, копирующей гигабайтный файл с радостным выводом итоговых IOPSов. Разумеется, это не имеет никакого отношения к реальным профилям нагрузки - но кого это волнует?
2. Выполняя тестирование, следует подходить к нему так же тщательно, как к полноценному IT-проекту: разработка технического задания, выполнение серий тестов, сбор и обработка результатов мониторинга, документирование результатов, принятие решения. Разумеется, следует привлекать квалифицированных специалистов (не одних лишь девопсов, разработчиков или эксплуатационников, для которых тестирование вообще лишняя головная боль и неоплачиваемая нагрузка), возможно, консультируясь с вендором.
3. Тестирование следует продумывать от начала и до конца - то есть до понимания его результатов. Системный мониторинг и его данные играют в этом ключевую роль. Если недостаточно собственной квалификации - следует привлекать квалифицированных специалистов извне (с чем могут быть серьезные сложности, учитывая исчезающую редкость специалистов по оптимизации производительности). Всегда приходится тщательно изучать документацию того, что тестируется, а во многих случаях и вникать глубже - вплоть до изучения поведения, архитектуры или исходного кода. Помимо понимания работы того, что, собственно говоря, тестируется - причем в комплексе.
4. Следует отдавать себе отчет, что вы, собственно говоря, тестируете, что для вас важно и что вам нужно увидеть. Отфонарные тесты по какому-то одному показателю - вообще не являются тестами. Понимание простых соотношений - что на что влияет, что на что может влиять, чего следует ожидать и на что смотреть - очень часто в принципе отсутствует.
Любые другие подходы, типа однократных прогонов и последующих зашакаленых скриншотов в телеграме - тестами не являются по определению. Это профанация самой идеи объективного тестирования.
С соответствующими результатами.
Кто виноват - понятно, что с этим делать и как получить сколько-нибудь достоверные результаты, если вы хоть сколько-нибудь в них заинтересованы? (привет отделам тестирования крупных банков)
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 с попытками дробления монолитных задач на слабосвязанные куски с тем, чтобы относительно единовременно исполнять их, по возможности, без синхронизации.
Как факт, индустрия сама себя загнала в технологический тупик, создав многоядерные процессоры, не имея для этого реальных оснований и теперь пытается хоть как-то оправдать их существование, породив апокалиптическую надстройку гиперинженирингового ПО в тщетных попытках как-то справиться с серьезнейшими алгоритмическими проблемами.
И транзакционная память не просто не решает этих проблем, а создает новые, имея, при этом, крайне ограниченное местечковое применение совсем не для средних умов.
Параллельное/конкурентное программирование - это сложно. Несмотря на то, что многоядерникам более 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 алгоритмы. С программной эмуляцией транзакционной памяти, конечно, можно поиграться, однако, чтобы тащить такое в продакшен, нужно быть очень смелым и очень глупым.
Вывод из сказанного следующий. Так как волшебной пули так и не изобрели, необходимо, вдумчиво используя традиционные средства (к примеру, экспоненциальный бэкофф в спинлоках не всегда благо), тщательно подходить к выбору микроархитектуры (в данном контексте - архитектуры программного кода) и к ее реализации. Медленно и тщательно. Как во времена Кернигана и Ричи. Только таким образом можно получить быстродействующий параллельный код.
Собственно говоря, транзакции являются несколько ортогональной концепцией применительно к 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. И существуют средства разрешить возникшую проблему.
Надо всего лишь о них знать. И слегка напрячь свою когнитивную сложность. Один раз. При разработке.
Оверхед таких порядков не игрушки. И, разумеется, вам никто из евангелистов не сознается, что он существует и имеет именно такие порядки.
И лишь вам решать - вам к умным или к красивым.
Откройте для себя 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 или Все дороги ведут в
Как вы думаете, зачем отцы-основатели, на чьих плечах мы лишь перхоть, раскидали библиотеки в системе по разным директориям?
и так далее?
И для чего изобрели
Отцам-основателям уже было известно, чем пахнут спелые фиги - DLL Hell. И они с ним достаточно успешно справлялись, отделяя мух от котлет. Вдумайтесь только - одна переменная среды или один конфиг
Однако вернемся к нашим барашкам.
И все было хорошо, с бородатыми сисадминами в уродливых свитерах с оленями на первоюниксах.
Однако некий мудрила, родом из пекла, не иначе, как по наущению дьявола, подкинул идейку - "А давайте все вообще библиотеки свалим в /lib. Вообще все. Системные, юзерлэндовские, приложений и пакетов. Ненуачо, удобнажы. И приколотим их гвоздями друг к другу и к libC, будем обновлять дистры как единое целое".
И разверзся портал в Ад.
У вас больше нет операционной системы общего назначения - с одной стороны, и кучки устанавливаемых пользователем приложений - с другой.
У вас есть 2048+ специализированных дистров для врачей-венерологов, водителей-дальнобойщиков, школьников младшего школьного возраста, школьников среднего школьного возраста...
В которых всё приколочено ко всему гвоздями-сотками. Сколько-нибудь серьезные обновления возможны лишь целиком - а вы попробуйте поставить в Дебиан 11 - OpenSSH 9.9 из Дебиан 12, не сломав всю систему к чертовой матери, попробуйте. Все депенденты в одной точке, разгрести эту кучу в стопочки не представляется возможным - что, кто-то из фанбоев любовно собирает сам, из исходников, с пеной у рта вытребываемых у разработчиков, свои сервисы и приложения, раскладывая библиотеки куда нужно и с пониманием, почему в большинстве пакетов префиксом по умолчанию является /usr/local?
В которых сколько-нибудь стабильная эксплуатация сколько-нибудь значимого и стабильного приложения за пределами LTS - как вам LTS в три года, кстати? Вы, наверное, в курсе, что в нормальном, не пластмассовом, мире LTS - это 10+ лет, не так ли? - возможна исключительно в контейнерах/песочницах. Доводы про безопасность контейнеров развеиваются, как дым - вы по факту замораживаете в контейнерах легаси. Что, серьезно? Обновлять целиком контейнер? То, что контейнеры по факту и изоляции-то сколько-нибудь полной не обеспечивают, как те же гипервизоры - ратующим за безопасность как-то не приходит в светлые головы. Не на том хардвере. Аппаратный
Вот так нежелание вдумчиво читать отцов-основателей, на чьих плечах мы лишь перхоть, и неудержимое желание реформаторства ради реформаторства, без желания сесть и задуматься - "Может быть, для того, что было сделано - были какие-то веские причины?" - и привело к тому, что мы имеем сейчас.
/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! 😉
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(). И использует его. Тогда аллокации можно слегка уменьшить. Но кто, положа руку на сердце, станет на это загоняться? Ну и так далее. Не смотрите вглубь - большое количество даже библиотечных функций являются неявно аллоцирующими (неявно - потому что - кто вообще будет читать их исходники?).
Кастомные аллокаторы принципиально не решают проблемы, будучи приложенными точечно. Потому что все остальные процессы рядом тупят - эффект шумных соседей. И - нет, микросервисы кардинально проблемы не решают. Это просто заметание проблемы под ковер. И - нет, серверлесс тоже не решает проблемы, потому что это маркетинговый термин, а код исполняется не на Великом Небесном Сервере, где-то в варпе, а на вполне реальном железе внутри вполне реальной ОС.
Как известно, под ковер никто не заглядывает.
Чтобы как-то значимо решить данную проблему, не переписывая весь код строка за строкой, нужна одна простая вещь.
Нужно глобально заменить системный аллокатор (и прочие велосипеды) на нечто более лучшее, но тоже общего назначения (это важно; кастомы глобально проблему не снимут и сколько-нибудь заметно ее не облегчат). Для этого это нечто нужно сперва разработать - и, как мы все понимаем, ничего сколько-нибудь высококачественного на гитхаб никто в здравом уме не выложит.
Со всем вышесказанным хорошо знакомы игроделы - и никто более, потому что всем остальным на уровень ниже фич глубоко начхать. Нет ни времени ни желания этим заниматься и даже просто задумываться.
Что ж, кто не хочет кормить своих специалистов - кормит чужих. И эксперты по оптимизации не совсем даром кушают свой хлеб.
Если, конечно, кто-то догадается их спросить - кто виноват и что делать.
Самые аллоцирующие задачи - это 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.
Резюмируя вышесказанное, как и во все времена, все определяется качеством алгоритмов. Не методика ХХП - а вдумчивое и тщательное кодирование, тщательная настройка и оптимизация операционного окружения, сбалансированное железо - вот ключ к быстрой и эффективной обработке данных.
По определению, 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.
Хотели убежать от указателей, да? Константная ссылка на инициализированный объект? Задумано-то неплохо, безопасность, тодасё. Однако - под капотом все тот же сырой указатель, и тут же сверху куча оберток
Другой пример - выравнивание. Откуда растут ноги? Ноги растут от аппаратной архитектуры. Процессор и его внутренние структуры оперируют машинным словом. А оперативная память адресуется побайтно. И тут начинаются дикие пляски с побайтным чтением из памяти, границей слова и прочим. С++17 наворачивает супервыравнивание в аллокациях new. Затем выясняется, что в многоядерниках с доступом к shared data в кэше надо бы выравнивать по размеру кэш-линии L1 процессора (вы правда думаете, что все на свете CPU имеют кэш-линию 64 бита?), в противном случае false sharing и трындец производительности в горячих местах в многопотоке. SIMD стоит отдельно и размахивает огромным красным знаменем.
Блестяще, мало того, что надо загоняться в выравнивание - его весьма часто надо по размеру кэш-линии выполнять. Комитет умнейшие ребята,
Выравнивание вообще отдельная печальная песня. Каждый раз, тыкаясь в память по указателю, на низком уровне надо проверять - выравнены ли данные? Это неслабый такой оверхэд. Спросите - на кой? Ну, как минимум, скорость доступа к невыровненным данным хуже. Как максимум, можно вообще словить UB на многих архитектурах.
В полном соответствии с Законом Протекающих Абстракций, комитет принимает assume aligned.
Ну, то есть, программист, как и в случае с кастами, говорит дословно - "Мамой клянусь, данные выровнены! Не генерируй проверку на выравнивание при доступах!".
Ладно, молчим про текущие абстракции, смотрим - реализовано? Да, реализовано, и давно - суперзелено!
Проверяем - GCC 15.1. Проверочного макроса нет. Вот вообще нет. От слова "совсем". Да, есть интринсик - причем ему сто лет в обед. Да, старые компиляторы интринсик поддерживают, понимают, и генерируют более эффективный ассемблер.
И - оппа! - свежие компиляторы мало того, что не содержат якобы реализованной функциональности. Они еще и интринсик молча игнорируют.
Как же так, Карл? Это же С++, ЯВУ, все низкоуровневые вещи должны быть давно попрятаны в подвал, нейроразнообразные должны фичи пилить, а не о железе и ассемблере думать. Компиляторы умные, они давно должны всё сами-сами-сами делать. Префетчи, restricted reference, assume aligned...
Что? Не выходит Каменный Цветок, Данила-мастер?
Не выходит. Либо думать над кодом и смотреть, что генерируется.
Либо застраивать планету датацентрами с тысячами серверов, исполняющих писанину на питоне и Go.
Всеобщая тенденция заменять шарящих на дешевых почему-то постоянно разбивается о Закон Протекающих Абстракций.
Возьмем, к примеру, комитет по стандартизации - хоть Си, хоть С++.
С одной стороны, идет перманентная движуха по наворачиванию всевозможных высокоуровневых штуковин. Для примера, хотя бы такая база, как 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.
Закон Протекающих Абстракций, часть 2
Оставим в покое компиляторы и компиляторщиков с их придурями.
Поговорим о памяти и фрагментации.
Если вспомнить о LBA, SSD и файловых системах - кто мешает сделать индексную адресацию последовательности байт физической памяти? И скрыть детали физической реализации, скажем, за сбалансированным B-деревом, как в базах данных? В которых путь от вершины до любого из листьев занимает O(1), а еще можно сделать преобразование адресов и на верхнем уровне представлять непрерывную последовательность (на кой это надо мы обсудим чуть позже).
Да, в общем-то, никто не мешает. Кроме двух вещей. Это медленно (подумайте о скоростях работы процессора и вообразите, что вы в самом горячем месте вертите деревья и пересчитываете адреса байт) и печально, а еще - дорого. Треть, а то и половину памяти займут - что? - метаданные. Их же надо где-то хранить, верно?
Даже в нынешние расточительные времена, когда в сервер можно влупить 24 Тб оперативы, оттяпать даже треть памяти под метаданные представляется безумием.
Вы скажете - окей, а как же виртуальная памяти? Она же, типа, транслирует виртуальные адреса в физические, ну и пусть заодно деревья вертит.
Не-а. У виртуальной памяти другое назначение. Она транслирует виртуальные адреса на совокупность адресов физической памяти плюс пространство подкачки. Это нужно для вытесняющей многозадачности, ну и, попутно, дефрагментация памяти через подкачку выполняется.
Что? Вы считаете подкачку ненужной и выключили ее? Поздравления, вы только что резко усилили фрагментацию, и, мало того, приблизили момент OOM и/или кернел паник.
Память на системном уровне выделяется страницами. Непрерывными последовательностями байт. Адресная арифметика. От которой не убежать. Почему? Потому, что процессор это машина Тьюринга, по большому счету (прости, Алан Мэтисонович). Оперирующая простейшими операциями. Нет, оперативная память это не хранилище с произвольным доступом (см.выше). На самом деле, на нижнем уровне мы оперируем указателями на байты и последовательности байт. Инкремент-декремент адресов и вот это вот всё. Накладывается на это тот факт, что мы оперируем буферами и файлами в буферах. С адресной арифметикой (вы правда надеялись избавиться от переполнения буфера и выхода за границы массива как концепции? Оп-па, абстракция снова потекла по усам Раста). Более-менее произвольный доступ по факту у нас лишь в списочных и графовых структурах из толпы указателей.
Соответственно, аллоцируя память, мы ищем непрерывные фрагменты нужного нам размера. И, на практике, очень скоро случается так, что суммарно свободных байт полно - но это пространство все нарезано мелкими кусочками. И мы, на очередной malloc внезапно получаем OOM. А то и вовсе кернел паник.
Технически, операционная система может выкрутиться из такой ситуации, сбросив в подкачку наиболее старые страницы и освободив место. Если у нее есть такая возможность.
Но, согласно современной моде, нет у нее такой возможности. Пусть падают сервисы, пусть хоть вся ось упадет. У нас есть кубер. Мы не дадим работать операционной системе так, как задумано; нам лучше знать, как должно быть. Плевать на оверинжиниринг, вообще на всё плевать. Бизнес заплатит, Боливару не снести двоих. Кого вообще волнует такая ерунда, как аптайм?
Собственно, натуралам хорошо известны бест-практисы, позволяющие избежать такой лютой дичи, как OOM Killer - и где! в ОС, позиционируемой как промышленная! Они просты, как три рубля: пространство подкачки (не файл, а раздел) обязано быть, иметь в размере не менее одного объема физической памяти, сколько бы этой физической памяти не было (помните? Дефрагментация).
Подкачка, к сожалению, не решает всех проблем, связанных с фрагментацией. Она снижает нарастание фрагментации со временем, но не предотвращает полностью. Более того, чем больше памяти на борту, тем быстрее она фрагментируется. ОС стремится освобожденные страницы памяти вернуть ядру, в общий фрагментированный пул свободной памяти. Мы же хотим RSS как можно меньше, верно?
Оставим в покое компиляторы и компиляторщиков с их придурями.
Поговорим о памяти и фрагментации.
Если вспомнить о LBA, SSD и файловых системах - кто мешает сделать индексную адресацию последовательности байт физической памяти? И скрыть детали физической реализации, скажем, за сбалансированным B-деревом, как в базах данных? В которых путь от вершины до любого из листьев занимает O(1), а еще можно сделать преобразование адресов и на верхнем уровне представлять непрерывную последовательность (на кой это надо мы обсудим чуть позже).
Да, в общем-то, никто не мешает. Кроме двух вещей. Это медленно (подумайте о скоростях работы процессора и вообразите, что вы в самом горячем месте вертите деревья и пересчитываете адреса байт) и печально, а еще - дорого. Треть, а то и половину памяти займут - что? - метаданные. Их же надо где-то хранить, верно?
Даже в нынешние расточительные времена, когда в сервер можно влупить 24 Тб оперативы, оттяпать даже треть памяти под метаданные представляется безумием.
Вы скажете - окей, а как же виртуальная памяти? Она же, типа, транслирует виртуальные адреса в физические, ну и пусть заодно деревья вертит.
Не-а. У виртуальной памяти другое назначение. Она транслирует виртуальные адреса на совокупность адресов физической памяти плюс пространство подкачки. Это нужно для вытесняющей многозадачности, ну и, попутно, дефрагментация памяти через подкачку выполняется.
Что? Вы считаете подкачку ненужной и выключили ее? Поздравления, вы только что резко усилили фрагментацию, и, мало того, приблизили момент OOM и/или кернел паник.
Память на системном уровне выделяется страницами. Непрерывными последовательностями байт. Адресная арифметика. От которой не убежать. Почему? Потому, что процессор это машина Тьюринга, по большому счету (прости, Алан Мэтисонович). Оперирующая простейшими операциями. Нет, оперативная память это не хранилище с произвольным доступом (см.выше). На самом деле, на нижнем уровне мы оперируем указателями на байты и последовательности байт. Инкремент-декремент адресов и вот это вот всё. Накладывается на это тот факт, что мы оперируем буферами и файлами в буферах. С адресной арифметикой (вы правда надеялись избавиться от переполнения буфера и выхода за границы массива как концепции? Оп-па, абстракция снова потекла по усам Раста). Более-менее произвольный доступ по факту у нас лишь в списочных и графовых структурах из толпы указателей.
Соответственно, аллоцируя память, мы ищем непрерывные фрагменты нужного нам размера. И, на практике, очень скоро случается так, что суммарно свободных байт полно - но это пространство все нарезано мелкими кусочками. И мы, на очередной malloc внезапно получаем OOM. А то и вовсе кернел паник.
Технически, операционная система может выкрутиться из такой ситуации, сбросив в подкачку наиболее старые страницы и освободив место. Если у нее есть такая возможность.
Но, согласно современной моде, нет у нее такой возможности. Пусть падают сервисы, пусть хоть вся ось упадет. У нас есть кубер. Мы не дадим работать операционной системе так, как задумано; нам лучше знать, как должно быть. Плевать на оверинжиниринг, вообще на всё плевать. Бизнес заплатит, Боливару не снести двоих. Кого вообще волнует такая ерунда, как аптайм?
Собственно, натуралам хорошо известны бест-практисы, позволяющие избежать такой лютой дичи, как OOM Killer - и где! в ОС, позиционируемой как промышленная! Они просты, как три рубля: пространство подкачки (не файл, а раздел) обязано быть, иметь в размере не менее одного объема физической памяти, сколько бы этой физической памяти не было (помните? Дефрагментация).
Подкачка, к сожалению, не решает всех проблем, связанных с фрагментацией. Она снижает нарастание фрагментации со временем, но не предотвращает полностью. Более того, чем больше памяти на борту, тем быстрее она фрагментируется. ОС стремится освобожденные страницы памяти вернуть ядру, в общий фрагментированный пул свободной памяти. Мы же хотим RSS как можно меньше, верно?
Закон Протекающих Абстракций, часть 3
Как ни странно, одним из корней зла является такая штука, как realloc.
Если вы думаете, что вас это не касается - попробуйте реализовать хоть какой-нибудь контейнер без реаллокаций. Realloc мало того, что одна из самых опасных функций libC. Она еще и одна из самых фрагментирующих. И медленных (нет, mremap не панацея. У нее куча оговорок и ограничений и она платформенно-специфична. Нет, оптимизировать memcpy дьявольски сложно и сделать лучше отцов-основателей никому не удавалось. Да, даже с учетом SIMD).
Что дальше-то? А ничего. Новомодные и не очень ЯП чхать хотели на текущие абстракции памяти - купите чумадан плашек. Вас сильно припекло? Напишите свою STL. Как фейсбук или Электроник Артс. Если сможете.
Можно попробовать написать свой аллокатор общего назначения. Что лишь кажется простенькой задачкой. Потому что он должен быть именно глобальным, и охватывать все краевые случаи. В идеале иметь сложность O(1) - мы же не хотим ждать неведомо сколько, пока операционка отхрюкает и найдет нам участок памяти нужного размера; и - нет, оверкоммит тут не помощник - и околонулевую фрагментацию.
Однако других вариантов просто нет. Игнорировать текущие абстракции вам не даст операционное окружение, а бьют они весьма больно. И - нет, замести это в контейнеры, свалить с больной головы на здоровую в serverless тоже не получится.
Ну, если, разумеется, вас не устраивает рестарт инжиниринг (а, помните, были времена, когда аптаймами мерялись?).
Менять сложившиеся абсолютно ублюдочные парадигмы разработки, разумеется, ради умников никто не станет. Равно как и переписывать халявные ОС, компиляторы и ЯП (хорошая попытка, Раст и Go, но нет). А равно как и тратить время на оптимизации кода - преждевременная оптимизация корень всех зол, а потом уже поздняк метадзе. Да и сисадмины толковые больше не нужны, всё есть код, нам лучше знать, с чего начинать.
Отсюда вытекает не менее ублюдочное понятие современной оптимизации производительности - завалим процессорными ядрами, чемоданами плашек памяти - а там запинаем. Вот и вся оптимизация. ДЦ размером с футбольные поля, гигаватты электричества. Производители лопат пьют до синевы за здоровье программистов.
А всего-то начинали с протекающих абстракций, которые было сравнительно легко купировать грамотной парадигмой и реализациями.
PS. Возможно, кого-то сильно удивит - однако даже микрооптимизации обладают синергическим эффектом. Если ими не пренебрегать, они в совокупности достаточно часто дают значимый эффект. Хотя бы в виде снижения утилизации CPU. Протекающими абстракциями довольно часто можно воспользоваться к всеобщей выгоде.
Как ни странно, одним из корней зла является такая штука, как realloc.
Если вы думаете, что вас это не касается - попробуйте реализовать хоть какой-нибудь контейнер без реаллокаций. Realloc мало того, что одна из самых опасных функций libC. Она еще и одна из самых фрагментирующих. И медленных (нет, mremap не панацея. У нее куча оговорок и ограничений и она платформенно-специфична. Нет, оптимизировать memcpy дьявольски сложно и сделать лучше отцов-основателей никому не удавалось. Да, даже с учетом SIMD).
Что дальше-то? А ничего. Новомодные и не очень ЯП чхать хотели на текущие абстракции памяти - купите чумадан плашек. Вас сильно припекло? Напишите свою STL. Как фейсбук или Электроник Артс. Если сможете.
Можно попробовать написать свой аллокатор общего назначения. Что лишь кажется простенькой задачкой. Потому что он должен быть именно глобальным, и охватывать все краевые случаи. В идеале иметь сложность O(1) - мы же не хотим ждать неведомо сколько, пока операционка отхрюкает и найдет нам участок памяти нужного размера; и - нет, оверкоммит тут не помощник - и околонулевую фрагментацию.
Однако других вариантов просто нет. Игнорировать текущие абстракции вам не даст операционное окружение, а бьют они весьма больно. И - нет, замести это в контейнеры, свалить с больной головы на здоровую в serverless тоже не получится.
Ну, если, разумеется, вас не устраивает рестарт инжиниринг (а, помните, были времена, когда аптаймами мерялись?).
Менять сложившиеся абсолютно ублюдочные парадигмы разработки, разумеется, ради умников никто не станет. Равно как и переписывать халявные ОС, компиляторы и ЯП (хорошая попытка, Раст и Go, но нет). А равно как и тратить время на оптимизации кода - преждевременная оптимизация корень всех зол, а потом уже поздняк метадзе. Да и сисадмины толковые больше не нужны, всё есть код, нам лучше знать, с чего начинать.
Отсюда вытекает не менее ублюдочное понятие современной оптимизации производительности - завалим процессорными ядрами, чемоданами плашек памяти - а там запинаем. Вот и вся оптимизация. ДЦ размером с футбольные поля, гигаватты электричества. Производители лопат пьют до синевы за здоровье программистов.
А всего-то начинали с протекающих абстракций, которые было сравнительно легко купировать грамотной парадигмой и реализациями.
PS. Возможно, кого-то сильно удивит - однако даже микрооптимизации обладают синергическим эффектом. Если ими не пренебрегать, они в совокупности достаточно часто дают значимый эффект. Хотя бы в виде снижения утилизации CPU. Протекающими абстракциями довольно часто можно воспользоваться к всеобщей выгоде.
Не мешай. Машине. Работать.
Как вы думаете, зачем деды придумали упорядочивать структуры по убыванию размеров мемберов? Чтобы ухудшить читаемость и усложнить жизнь потомкам?
Нет. Такое неочевидное выравнивание (это может показаться глупым современным программистам) позволяет за счет минимизации паддинга уменьшить размер структуры. Так как структуры, как правило, являются элементами контейнеров - зачастую, нешуточных - это уменьшает потребность в памяти. Отсюда же растут ноги у прагмы упаковки структуры.
Как и во всех случаях, дилемма "либо память - либо скорость" справедлива не только в отношении древних одноядерников, но и - в особенности - в случае многоядерных современных процессоров.
Есть такая штука, как кэш-линия L1. И - нет, она далеко не на всех процессорах имеет размер 8 байт.
Флаг блокировки (мы, разумеется, говорим про атомики) может оказаться в одной кэш-линии с другим shared мембером структуры. Что, в случае тредов, может запросто привести к такой неприятной и малоизвестной вещи, как false sharing. Которая, как следствие, при высокой конкурентности может привести к резкому росту внутрипроцессорного трафика и падению производительности.
Лечится это не просто, а относительно просто. Надо всего лишь выровнять флаг блокировки на размер кэш-линии процессора (успехов в его определении; там свои заморочки), после чего он окажется один на кэш-линию (с паддингом) и проблема false sharing будет практически решена.
И обратите внимание вот на что. Выравнивание самой структуры != выравниванию всех ее полей по кэш-линиям. Они могут быть выровнены - в зависимости от размеров-битности сборки-кэш-линии. А могут и не быть.
Однако, у всего есть своя цена. Итоговый размер структуры несколько вырастет. Может ощутимо вырасти. С соответствующими последствиями. Так что это trade off, который, кстати говоря, касается не только флагов блокировки, но и других членов структуры.
Ситуация усложняется с использованием мьютексов. Вы, без сомнения, в курсе, что
С учетом вышесказанного, делать мьютекс частью структуры - идея так себе. Да, и false sharing. И выравнивание субструктуры проблемное.
По этой причине, если вас хоть сколько-нибудь волнует производительность, мьютексы логично вынести за пределы каких бы то ни было структур и сделать глобальными. При возможности. В противном случае, к органическим проблемам мьютексов добавятся вышеописанные эффекты. Которые, конечно, игнорируются программистами, что и приводит, как следствие, к заваливанию проблем кода железом и котлетами денег.
Финализируя вышесказанное, читаемость кода - это не критерий его качества. Конечно, возможность понимания даже людьми, обученными по пошаговым туториалам, дорогого стоит. Однако, возможно, им просто следовало избрать другую профессию?
Все же код, прежде всего, должен быть хорош для того, кто его исполняет.
Да и комментарии уже минимум полстолетия как изобрели.
Как вы думаете, зачем деды придумали упорядочивать структуры по убыванию размеров мемберов? Чтобы ухудшить читаемость и усложнить жизнь потомкам?
Нет. Такое неочевидное выравнивание (это может показаться глупым современным программистам) позволяет за счет минимизации паддинга уменьшить размер структуры. Так как структуры, как правило, являются элементами контейнеров - зачастую, нешуточных - это уменьшает потребность в памяти. Отсюда же растут ноги у прагмы упаковки структуры.
Как и во всех случаях, дилемма "либо память - либо скорость" справедлива не только в отношении древних одноядерников, но и - в особенности - в случае многоядерных современных процессоров.
Есть такая штука, как кэш-линия L1. И - нет, она далеко не на всех процессорах имеет размер 8 байт.
Флаг блокировки (мы, разумеется, говорим про атомики) может оказаться в одной кэш-линии с другим shared мембером структуры. Что, в случае тредов, может запросто привести к такой неприятной и малоизвестной вещи, как false sharing. Которая, как следствие, при высокой конкурентности может привести к резкому росту внутрипроцессорного трафика и падению производительности.
Лечится это не просто, а относительно просто. Надо всего лишь выровнять флаг блокировки на размер кэш-линии процессора (успехов в его определении; там свои заморочки), после чего он окажется один на кэш-линию (с паддингом) и проблема false sharing будет практически решена.
И обратите внимание вот на что. Выравнивание самой структуры != выравниванию всех ее полей по кэш-линиям. Они могут быть выровнены - в зависимости от размеров-битности сборки-кэш-линии. А могут и не быть.
Однако, у всего есть своя цена. Итоговый размер структуры несколько вырастет. Может ощутимо вырасти. С соответствующими последствиями. Так что это trade off, который, кстати говоря, касается не только флагов блокировки, но и других членов структуры.
Ситуация усложняется с использованием мьютексов. Вы, без сомнения, в курсе, что
std::mutex это обертка над pthread_mutex_t, который совсем не POD. А структура, причем структура, специфическая для конкретной платформы в смысле реализации.С учетом вышесказанного, делать мьютекс частью структуры - идея так себе. Да, и false sharing. И выравнивание субструктуры проблемное.
По этой причине, если вас хоть сколько-нибудь волнует производительность, мьютексы логично вынести за пределы каких бы то ни было структур и сделать глобальными. При возможности. В противном случае, к органическим проблемам мьютексов добавятся вышеописанные эффекты. Которые, конечно, игнорируются программистами, что и приводит, как следствие, к заваливанию проблем кода железом и котлетами денег.
Финализируя вышесказанное, читаемость кода - это не критерий его качества. Конечно, возможность понимания даже людьми, обученными по пошаговым туториалам, дорогого стоит. Однако, возможно, им просто следовало избрать другую профессию?
Все же код, прежде всего, должен быть хорош для того, кто его исполняет.
Да и комментарии уже минимум полстолетия как изобрели.
Скрытая угроза
Манера удалять не глядя coredumps без исследования, равно как и недостаточное внимание к работе с памятью, эффективно скрывают такую устрашающую вещь, как UAF - use after free (а еще DF - double free, heap corruption).
Что, собственно, происходит, когда вы освобождаете память free/delete? В нормальной системе искаропки, поставленной по принципу Next-next-next-finish? (А, по сути, в любой системе)
Правильно. Указатель на блок памяти попадает в очередь освободившихся блоков ОС, вместе с этим блоком памяти. Память не очищается, если вы сами ее не почистите перед освобождением. Указатель продолжает оставаться валидным, если вы сами его не занулите (не присвоите nullptr, если мы говорим о C++).
Это нормальное поведение (ОС, вообще говоря, в приоритете имеет несколько иные вещи, чем сборка мусора), которое, однако не описывается жирным шрифтом в руководствах (которых, разумеется, никто не читает). Причем мы говорим про вообще любые ОС, кроме, может быть ОС РВ. И это не баг, не недосмотр - это нормальное документированное поведение.
В этой очереди освобожденные блоки могут оставаться достаточно долго, и блок доступен.
Вы можете сказать "Ха! И еще раз - ха! Умные указатели уже изобрели!"
Да, их изобрели. Однако - надо читать стандарт и реализации внимательно -
Заметьте вот что.
Первое - такие вещи чрезвычайно трудно отловить. Статический анализ их почти не способен выловить (кроме самых очевидных случаев). Отловить их (в некоторых случаях) способен кастомный аллокатор, не имеющий очередей и немедленно, честно освобождающий блоки. На уровне своих метаданных. Он, по крайней мере, посредством кордампа, вам просигналит, что вот это вот приложение имеет в себе опасный баг (а вы его в прод затащили, просто потому, что - да откуда бы вам знать, что там UAF? Код, что ли, читать? Еще чего, пусть миллион мух напрягается. Да и не особо поможет чтение, справедливости ради).
Второе - раст не спасёт. Под ним все тот же рантайм Си, а под ним ядро ОС. Что происходит вне раста - не проблема раста.
Третье - языки со сборщиками мусора это хорошо, конечно. Если написано без ошибок. Но адски медленно. Так как закон Мура почил в бозе, нам уже не настолько все равно на производительность кода.
Резюме неутешительно. "ТщательнЕй надо, ребята. ТщательнЕй!"
Данный класс ошибок - это безопасность. Прямая дорога к зеродэям. Их нельзя просто проигнорировать и пусть падает, кубернетес под перезапустит.
А для начала, об этом просто надо знать.
PS. Практический опыт показывает, что приложений с такими багами - релизных, в продакшене - почти 30-40%. Причем среди них находится, в частности, такой рекордсмен сетапов, как OpenSSL/LibreSSL. Вот теперь можете начинать бояться.
Манера удалять не глядя coredumps без исследования, равно как и недостаточное внимание к работе с памятью, эффективно скрывают такую устрашающую вещь, как UAF - use after free (а еще DF - double free, heap corruption).
Что, собственно, происходит, когда вы освобождаете память free/delete? В нормальной системе искаропки, поставленной по принципу Next-next-next-finish? (А, по сути, в любой системе)
Правильно. Указатель на блок памяти попадает в очередь освободившихся блоков ОС, вместе с этим блоком памяти. Память не очищается, если вы сами ее не почистите перед освобождением. Указатель продолжает оставаться валидным, если вы сами его не занулите (не присвоите nullptr, если мы говорим о C++).
Это нормальное поведение (ОС, вообще говоря, в приоритете имеет несколько иные вещи, чем сборка мусора), которое, однако не описывается жирным шрифтом в руководствах (которых, разумеется, никто не читает). Причем мы говорим про вообще любые ОС, кроме, может быть ОС РВ. И это не баг, не недосмотр - это нормальное документированное поведение.
В этой очереди освобожденные блоки могут оставаться достаточно долго, и блок доступен.
Вы можете сказать "Ха! И еще раз - ха! Умные указатели уже изобрели!"
Да, их изобрели. Однако - надо читать стандарт и реализации внимательно -
unique_ptr, например, при выходе из скоупа указатель не зануляет. Это полбеды. Где-нибудь в середине кода вы можете вынуть из него сырые данные посредством std::data, присвоить это содержимое другому указателю. И благополучно про него забыть - а он не будет автоматически очищен деструктором. Дело осложняется тем, что в unique_ptr может лежать, к примеру, массив сырых указателей. На что угодно. И их надо либо освобождать кастомным деструктором, либо отдельно зачищать. Забыли? Получите утечку памяти - в лучшем случае, в худшем - UAF или DF.shared_ptr, конечно, со своим счетчиком ссылок, спасает гигантов мысли. Но вы представляете, да, насколько он тяжелее unique_ptr? И во что выльется простая лобовая замена - можете себе представить. Всё плохо.Заметьте вот что.
Первое - такие вещи чрезвычайно трудно отловить. Статический анализ их почти не способен выловить (кроме самых очевидных случаев). Отловить их (в некоторых случаях) способен кастомный аллокатор, не имеющий очередей и немедленно, честно освобождающий блоки. На уровне своих метаданных. Он, по крайней мере, посредством кордампа, вам просигналит, что вот это вот приложение имеет в себе опасный баг (а вы его в прод затащили, просто потому, что - да откуда бы вам знать, что там UAF? Код, что ли, читать? Еще чего, пусть миллион мух напрягается. Да и не особо поможет чтение, справедливости ради).
Второе - раст не спасёт. Под ним все тот же рантайм Си, а под ним ядро ОС. Что происходит вне раста - не проблема раста.
Третье - языки со сборщиками мусора это хорошо, конечно. Если написано без ошибок. Но адски медленно. Так как закон Мура почил в бозе, нам уже не настолько все равно на производительность кода.
Резюме неутешительно. "ТщательнЕй надо, ребята. ТщательнЕй!"
Данный класс ошибок - это безопасность. Прямая дорога к зеродэям. Их нельзя просто проигнорировать и пусть падает, кубернетес под перезапустит.
А для начала, об этом просто надо знать.
PS. Практический опыт показывает, что приложений с такими багами - релизных, в продакшене - почти 30-40%. Причем среди них находится, в частности, такой рекордсмен сетапов, как OpenSSL/LibreSSL. Вот теперь можете начинать бояться.
TheCacheWorks pinned «Скрытая угроза Манера удалять не глядя coredumps без исследования, равно как и недостаточное внимание к работе с памятью, эффективно скрывают такую устрашающую вещь, как UAF - use after free (а еще DF - double free, heap corruption). Что, собственно, происходит…»
Ядра чистый изумруд
Тот факт, что логические ядра совсем не эквивалент физическим и что HT/SMT это чистый маркетинг, было понятно любому здоровому человеку еще на момент появления этой технологии.
Попытка объехать заглохший закон Мура на гнилой козе аппаратно, конечно, достойна всяческого уважения - хотя бы потому, что буллшит умудрились-таки втюхать индустрии на десятилетия - но тщетная.
Дело, собственно говоря, в следующем.
Задач, которые HT/SMT хотя бы немного ускоряет (совсем не вдвое) - по пальцам одной руки пересчитать.
Некоторое количество однопоточного - обратите внимание, однопоточного - кода, конечно, ускоряется. Процентов на 20-30. В самом идеальном случае.
Однако с появлением тредового кода в значимых количествах специалистам по оптимизации, в общем, довольно быстро стало ясно, что реальные треды к якобы аппаратным тредам имеют весьма опосредованное отношение.
Что представляют собой логические ядра? Простым языком, это некоторые могущие простаивать во время выполнения аппаратные блоки физических ядер. Которые опознаются как полные процессорные ядра, однако полными процессорными ядрами по факту не являющиеся.
По факту, на тредовых задачах HT/SMT лишь увеличивает конкуренцию за кэши и порты (внутрипроцессорную), путается под ногами у NUMA, и провоцирует проблемы с безопасностью (здравствуйте, Meltdown&Spectre).
Реальные тредовые тесты показывают более, чем двукратное снижение производительности тредовых приложений на HT/SMT.
Проблема на уровне программирования осложняется тем фактом, что отличить логические "ядра" от реальных физических весьма непросто -
Отдельная печаль - это поядерное лицензирование софта. Хотя логические ядра != физическим, очень многие вендоры с этим не согласны и на голубом глазу выставляют лицензии по числу логических ядер.
Очень показателен пример процессора SPARC M8. Заявленная тактовая частота которого составляет 4,5 ГГц, имеющего 32 физических ядра по 8 аппаратных тредов на ядро, что в сумме составляет впечатляющие, на первый взгляд, 256 ядер на сокет. Увы и ах - реальная скорость этих логических ядер совсем не равна частоте процессора, она много ниже. А скорость тредовых приложений падает просто драматически при выполнении в таком режиме работы CPU.
Однако наивные пользователи свято верят, что имеют 256 ядер. А потом горько плачут, когда реальная производительность на этих ядрах оказывается просто удручающей.
Опыт показывает, что в подавляющем большинстве серверных приложений HT/SMT не просто не нужны, а откровенно вредят. И их надо отключать, если вы хотите получить хоть сколько-нибудь приемлемую лейтенси. Если это отключение вообще возможно (подсказка - совсем не всегда).
А специалистов по производительности просто никто не удосужился спросить и хоть как-то принять во внимание их мнение.
Так и получилось, что в высшей степени сомнительные в своей эффективности решения на десятилетия заняли совершенно неподобающее место в мозгах и серверных.
Тот факт, что логические ядра совсем не эквивалент физическим и что HT/SMT это чистый маркетинг, было понятно любому здоровому человеку еще на момент появления этой технологии.
Попытка объехать заглохший закон Мура на гнилой козе аппаратно, конечно, достойна всяческого уважения - хотя бы потому, что буллшит умудрились-таки втюхать индустрии на десятилетия - но тщетная.
Дело, собственно говоря, в следующем.
Задач, которые HT/SMT хотя бы немного ускоряет (совсем не вдвое) - по пальцам одной руки пересчитать.
Некоторое количество однопоточного - обратите внимание, однопоточного - кода, конечно, ускоряется. Процентов на 20-30. В самом идеальном случае.
Однако с появлением тредового кода в значимых количествах специалистам по оптимизации, в общем, довольно быстро стало ясно, что реальные треды к якобы аппаратным тредам имеют весьма опосредованное отношение.
Что представляют собой логические ядра? Простым языком, это некоторые могущие простаивать во время выполнения аппаратные блоки физических ядер. Которые опознаются как полные процессорные ядра, однако полными процессорными ядрами по факту не являющиеся.
По факту, на тредовых задачах HT/SMT лишь увеличивает конкуренцию за кэши и порты (внутрипроцессорную), путается под ногами у NUMA, и провоцирует проблемы с безопасностью (здравствуйте, Meltdown&Spectre).
Реальные тредовые тесты показывают более, чем двукратное снижение производительности тредовых приложений на HT/SMT.
Проблема на уровне программирования осложняется тем фактом, что отличить логические "ядра" от реальных физических весьма непросто -
std::thread::hardware_concurrency(), например, их не различает. Ну и мир не ограничивается архитектурами x86/ARM, и cpuid есть совсем не в каждом процессоре на планете.Отдельная печаль - это поядерное лицензирование софта. Хотя логические ядра != физическим, очень многие вендоры с этим не согласны и на голубом глазу выставляют лицензии по числу логических ядер.
Очень показателен пример процессора SPARC M8. Заявленная тактовая частота которого составляет 4,5 ГГц, имеющего 32 физических ядра по 8 аппаратных тредов на ядро, что в сумме составляет впечатляющие, на первый взгляд, 256 ядер на сокет. Увы и ах - реальная скорость этих логических ядер совсем не равна частоте процессора, она много ниже. А скорость тредовых приложений падает просто драматически при выполнении в таком режиме работы CPU.
Однако наивные пользователи свято верят, что имеют 256 ядер. А потом горько плачут, когда реальная производительность на этих ядрах оказывается просто удручающей.
Опыт показывает, что в подавляющем большинстве серверных приложений HT/SMT не просто не нужны, а откровенно вредят. И их надо отключать, если вы хотите получить хоть сколько-нибудь приемлемую лейтенси. Если это отключение вообще возможно (подсказка - совсем не всегда).
А специалистов по производительности просто никто не удосужился спросить и хоть как-то принять во внимание их мнение.
Так и получилось, что в высшей степени сомнительные в своей эффективности решения на десятилетия заняли совершенно неподобающее место в мозгах и серверных.
UB or not UB
That is the serious question.
Большинство программистов как чумы боятся высоких уровней оптимизации. "Оно начинает рандомно дампить".
Нет, господа. Нет, Гэри.
Это не копиляторы кривые. Это ваш код кривой.
Высокие уровни оптимизации (выше -O2) вытаскивают из-под ковра на свет божий все UB, скрытые и не очень.
Компиляторщики пытаются усидеть на двух стульях - они пытаются агрессивно оптимизировать те навороченые многослойные абстракции, которые вы пишете. И в то же время, их клюют за безопасность памяти.
Безопасность памяти это вообще отдельная тема.
Вы, наверное, в курсе, что обеспечение этой самой безопасности - это не прерогатива компилятора (здравствуй, Раст! напишите без unsafe что-то сколько-нибудь сложное, быстрое и интегрируемое - а, главное, обойдите рантайм libC при этом), не прерогатива системного (или несистемного) аллокатора памяти (и не надо ждать от него несвойственных ему функций, он не обязан ловить ваши DF/UAF/heap corruption - вообще, от слова "абсолютно". Он в таких случаях обязан с грохотом упасть в кордамп и не более того - и - нет, не замести ошибку под ковер).
Это прерогатива программиста.
В противном случае - прощай, производительность.
Разумеется, это требует вдумчивого, неспешного кодирования. Чтобы быстро бегать - надо медленно идти.
Что идет абсолютно вразрез с мейнстримными методологиями разработки. В глобальных масштабах методологии TTM оборачиваются пожиганием электроэнергии в колоссальных количествах, сравнимых с блокчейном и недоИИ - но всем, разумеется, пофигу, пока счета за электроэнергию не превысят доход.
Anyway.
Хороший, высокопроизводительный, качественный код всегда шагает по краю UB. Но при этом никогда за него не заступает.
Это требует тщательного, вдумчивого кодирования, и не менее тщательного тестирования.
Однако в долгосроке всегда окупается лучше, нежели TTM.
PS. Информация к размышлению - разница в производительности кода, собранного с уровнем оптимизации -O2 и -O3 может составлять до 30%.
That is the serious question.
Большинство программистов как чумы боятся высоких уровней оптимизации. "Оно начинает рандомно дампить".
Нет, господа. Нет, Гэри.
Это не копиляторы кривые. Это ваш код кривой.
Высокие уровни оптимизации (выше -O2) вытаскивают из-под ковра на свет божий все UB, скрытые и не очень.
Компиляторщики пытаются усидеть на двух стульях - они пытаются агрессивно оптимизировать те навороченые многослойные абстракции, которые вы пишете. И в то же время, их клюют за безопасность памяти.
Безопасность памяти это вообще отдельная тема.
Вы, наверное, в курсе, что обеспечение этой самой безопасности - это не прерогатива компилятора (здравствуй, Раст! напишите без unsafe что-то сколько-нибудь сложное, быстрое и интегрируемое - а, главное, обойдите рантайм libC при этом), не прерогатива системного (или несистемного) аллокатора памяти (и не надо ждать от него несвойственных ему функций, он не обязан ловить ваши DF/UAF/heap corruption - вообще, от слова "абсолютно". Он в таких случаях обязан с грохотом упасть в кордамп и не более того - и - нет, не замести ошибку под ковер).
Это прерогатива программиста.
В противном случае - прощай, производительность.
Разумеется, это требует вдумчивого, неспешного кодирования. Чтобы быстро бегать - надо медленно идти.
Что идет абсолютно вразрез с мейнстримными методологиями разработки. В глобальных масштабах методологии TTM оборачиваются пожиганием электроэнергии в колоссальных количествах, сравнимых с блокчейном и недоИИ - но всем, разумеется, пофигу, пока счета за электроэнергию не превысят доход.
Anyway.
Хороший, высокопроизводительный, качественный код всегда шагает по краю UB. Но при этом никогда за него не заступает.
Это требует тщательного, вдумчивого кодирования, и не менее тщательного тестирования.
Однако в долгосроке всегда окупается лучше, нежели TTM.
PS. Информация к размышлению - разница в производительности кода, собранного с уровнем оптимизации -O2 и -O3 может составлять до 30%.
Nice to have
Как вам, должно быть, известно, в оптимизации производительности мелочей нет.
Даже микрооптимизации способны давать синергический эффект, если на них не забивать.
В частности, это касается такой, казалось бы, обыденной вещи, как фичафлаги второ- и третьестепенных функций. Которые nice to have, а не must to have.
Посмотрим на них внимательней - что они представляют из себя.
Как правило, это рассыпаные по коду ifы с булевским флагом.
Казалось бы, что может пойти не так?
Да всё вообще. Нежданный if, который нужно выполнять при каждом проходе по коду, это ассемблерный джамп. Который нарушает последовательность выполнения и опустошает конвейер предвыборки процессорного ядра.
В общем и целом, такой дополнительный if в горячем месте может съесть в среднем до 10% от производительности. Иногда и больше.
Можно ли с этим что-то сделать?
В идеале - условная компиляция. Да, фичи на хардкоде. Да, это противоречит позиции Комитета "шаблоны вместо макросов, макросы однозначное зло". Сервер, конечно же, железянный, но законы физики никто не отменял. Нет кода - нет проблем.
Но - это идеал. Никакого лишнего кода, если он не нужен.
Как быть, если он все-таки нужен и надо что-нибудь втереть, или вправить или еще что-нибудь?
Первое - поможет короткое замыкание. Если проверка условия фичафлага такое допускает (иногда такое бывает в динамических фичафлагах). Считаем минимум.
Второе - хинт expect. Несмотря на многократный shame на SO, он помогает довольно эффективно купировать лишнее ветвление. Работает это на уровне компиляции. Компилятор более вероятную ветвь просто распрямляет, переставляя джамп в другое место, с тем, чтобы конвейер предвыборки команд процессора по-возможности не опустошался.
Тут есть лишь одна сложность - как определить наиболее вероятную ветвь. Поможет небольшой временный вспомогательный код со счетчиком переходов в обе ветви). А также микробенчмаркинг и тестирование.
И третье - иногда помогает вдумчивое реструктурирование кода с ветвлениями. Да, вплоть до рефакторинга. Меньше ifов - выше скорость.
Если резюмировать все вышесказанное, то вывод снова неутешительный: "Тщательней надо, ребята! Тщательней!"
Ну и еще раз хорошенько подумать - а нужно ли превращать проект в Nero Burning ROM.
Вдумчивое кодирование позволит коду выполняться быстрее, хоть и принципиально противоречит TTM. И имена разработчиков не будут полоскать в связи с нецензурными эпитетами.
Как вам, должно быть, известно, в оптимизации производительности мелочей нет.
Даже микрооптимизации способны давать синергический эффект, если на них не забивать.
В частности, это касается такой, казалось бы, обыденной вещи, как фичафлаги второ- и третьестепенных функций. Которые nice to have, а не must to have.
Посмотрим на них внимательней - что они представляют из себя.
Как правило, это рассыпаные по коду ifы с булевским флагом.
Казалось бы, что может пойти не так?
Да всё вообще. Нежданный if, который нужно выполнять при каждом проходе по коду, это ассемблерный джамп. Который нарушает последовательность выполнения и опустошает конвейер предвыборки процессорного ядра.
В общем и целом, такой дополнительный if в горячем месте может съесть в среднем до 10% от производительности. Иногда и больше.
Можно ли с этим что-то сделать?
В идеале - условная компиляция. Да, фичи на хардкоде. Да, это противоречит позиции Комитета "шаблоны вместо макросов, макросы однозначное зло". Сервер, конечно же, железянный, но законы физики никто не отменял. Нет кода - нет проблем.
Но - это идеал. Никакого лишнего кода, если он не нужен.
Как быть, если он все-таки нужен и надо что-нибудь втереть, или вправить или еще что-нибудь?
Первое - поможет короткое замыкание. Если проверка условия фичафлага такое допускает (иногда такое бывает в динамических фичафлагах). Считаем минимум.
Второе - хинт expect. Несмотря на многократный shame на SO, он помогает довольно эффективно купировать лишнее ветвление. Работает это на уровне компиляции. Компилятор более вероятную ветвь просто распрямляет, переставляя джамп в другое место, с тем, чтобы конвейер предвыборки команд процессора по-возможности не опустошался.
Тут есть лишь одна сложность - как определить наиболее вероятную ветвь. Поможет небольшой временный вспомогательный код со счетчиком переходов в обе ветви). А также микробенчмаркинг и тестирование.
И третье - иногда помогает вдумчивое реструктурирование кода с ветвлениями. Да, вплоть до рефакторинга. Меньше ifов - выше скорость.
Если резюмировать все вышесказанное, то вывод снова неутешительный: "Тщательней надо, ребята! Тщательней!"
Ну и еще раз хорошенько подумать - а нужно ли превращать проект в Nero Burning ROM.
Вдумчивое кодирование позволит коду выполняться быстрее, хоть и принципиально противоречит TTM. И имена разработчиков не будут полоскать в связи с нецензурными эпитетами.