Это кастомизируемый набор метрик, отражающий "сложность сцены". Например, на графике времени кадра мы видим что апдейт UI занимает слишком много времени. Но почему? Как правило это либо косяки в логике, либо просто на сцене дофига UI элементов
В целом, большинство проблем в unity-like движках, это раздутая и не оптимизировання сцена, где каждая горошина проработана и рендерится. Нууу потому что вот так вот, потому что пофиг..
Это сразу видно в виджете и здесь так же используется цветовая дифференциация. Если метрика стала красной, то может быть это то самое проблемное место. Как в примере с долгим апдейтом UI, может оказаться что у нас много Images и Nodes.
Важно отметить, что если одна или несколько метрик стали красными - это не значит что 100% перфомансу похерело. Это стоит воспринимать как эвристику, помогающую базово понять, есть ли проблема. Если на сцене 20 тысяч нод, но FPS стабильно на 60, значит все нормально. Ведь финальная метрика - это FPS
Пороговые значения настраиваются в конфиге "опытным разработчиком на основе многолетней экспертизы". Тобишь пальцем в небо... Но это нормально, ведь эти матрики косвенные и не утверждают что та или иная метрика точно портит перфоманс. Это скорее как набор анализов, на основе которых разработчик выносит диагноз
В целом, большинство проблем в unity-like движках, это раздутая и не оптимизировання сцена, где каждая горошина проработана и рендерится. Нууу потому что вот так вот, потому что пофиг..
Это сразу видно в виджете и здесь так же используется цветовая дифференциация. Если метрика стала красной, то может быть это то самое проблемное место. Как в примере с долгим апдейтом UI, может оказаться что у нас много Images и Nodes.
Важно отметить, что если одна или несколько метрик стали красными - это не значит что 100% перфомансу похерело. Это стоит воспринимать как эвристику, помогающую базово понять, есть ли проблема. Если на сцене 20 тысяч нод, но FPS стабильно на 60, значит все нормально. Ведь финальная метрика - это FPS
Пороговые значения настраиваются в конфиге "опытным разработчиком на основе многолетней экспертизы". Тобишь пальцем в небо... Но это нормально, ведь эти матрики косвенные и не утверждают что та или иная метрика точно портит перфоманс. Это скорее как набор анализов, на основе которых разработчик выносит диагноз
src.zip
15.7 KB
Весь этот набор инструментов, конечно же, не заменяет более продвинутые профалйлеры, типа tracy, xcode, vtune. Это скорее как первая линия, чтобы минимально проанализировать ситуацию и проконтролировать, что не было сделано грубых ошибок.
Некоторые проблемы можно обнаружить сразу, открыв этот виджет: раздутую сцену, слишком сложную логику в визуальном скрипте или тажелый рендер. И уже исходя из этого пытаться ее лечить
А если не понятно до конца в чем именно проблема, то он поможет значительно сузить зону поиска и уже переходить к более точным и сложным способам профайлинга
Приложу архивчик с исходниками. Они здесь as is, то есть никакой универсальности нет, но их можно использовать как основу, чтобы сделать что-то подобное у себя
Некоторые проблемы можно обнаружить сразу, открыв этот виджет: раздутую сцену, слишком сложную логику в визуальном скрипте или тажелый рендер. И уже исходя из этого пытаться ее лечить
А если не понятно до конца в чем именно проблема, то он поможет значительно сузить зону поиска и уже переходить к более точным и сложным способам профайлинга
Приложу архивчик с исходниками. Они здесь as is, то есть никакой универсальности нет, но их можно использовать как основу, чтобы сделать что-то подобное у себя
This media is not supported in your browser
VIEW IN TELEGRAM
тащусь с этой прикормки 🤩
Monosnap Video 2025-06-10 20.1.gif
83.5 MB
В интересное время живем! Когда я смотрел фильмы, фантастику, и там герои смотрели видео и потом такие "а теперь посмотрим с другого угла", я думал - ну и чушь полнейшая... Но вот он сайт - 4dv.ai, и в нем можно сделать именно так. Интересно, станут ли наши видео трехмерными? Или нафиг не нужно?
Нет, не будем обсуждать какие есть и какие из них лучше. Этому уже посвящено куча книг и холиваров. А поговорим про их суть, как они работают, и неочевидные места их применения. На мой взгляд первично именно понимание, а применение уже производное: если ты знаешь как это работает, ты лучше понимаешь как это применять или сделать что-то свое.
Окей, что такое паттерн? Это универсальное слово, оно обозначает что-то повторяющееся, что-то знакомое. Использующееся в разных областях, не только в программировании, но и в дизайне, например.
Мы любим в программировании применять паттерны. Но зачастую это работает по принципу "паттерны - это хорошо, применяем паттерны везде", без осознания что конкретно они дают. Часто программисты пересекают черту, где пора перестать упарываться и втыкать все известные паттерны в код. Это порождает оверинжиниринг
Что ж, чтобы понять как правильно их юзать, нужно понять их суть, как они работают. Я бы выделил тут два аспекта:
- паттерн - это что-то узнаваемое среди массы людей. То есть они уже видели такое, знают как оно устроено и легко узнают. И у разных людей очень похожее понимание этой сути
- так же паттерн - это нечто повторяющееся. В целом это можно сказать то же, что и первый пункт, мозгу гораздо легче воспринять что-то знакомое и повторяющееся, чем тратить энергию на осознание чего-то нового
Все это артефакты устройства нашего мозга, а точнее его ограниченности. У нас ограниченный контекст и возможность воспринимать новую информацию. Все ведь помнят как в школе/институте под конец дня новые знания превращались в кашу, хотя с утра все шло довольно легко? А еще, небольшой тест: представьте себе 10 отдельных людей, со всеми деталями и движениями. Не получится, мы просто не можем держать такой большой контекст одновременно. Обычно предел где-то около 5-6. Но вот само понятие "10 людей", без детализации, уже довольно простое
Учитывая эти особенности, можно уже подумать как правильно применять паттерны
Окей, что такое паттерн? Это универсальное слово, оно обозначает что-то повторяющееся, что-то знакомое. Использующееся в разных областях, не только в программировании, но и в дизайне, например.
Мы любим в программировании применять паттерны. Но зачастую это работает по принципу "паттерны - это хорошо, применяем паттерны везде", без осознания что конкретно они дают. Часто программисты пересекают черту, где пора перестать упарываться и втыкать все известные паттерны в код. Это порождает оверинжиниринг
Что ж, чтобы понять как правильно их юзать, нужно понять их суть, как они работают. Я бы выделил тут два аспекта:
- паттерн - это что-то узнаваемое среди массы людей. То есть они уже видели такое, знают как оно устроено и легко узнают. И у разных людей очень похожее понимание этой сути
- так же паттерн - это нечто повторяющееся. В целом это можно сказать то же, что и первый пункт, мозгу гораздо легче воспринять что-то знакомое и повторяющееся, чем тратить энергию на осознание чего-то нового
Все это артефакты устройства нашего мозга, а точнее его ограниченности. У нас ограниченный контекст и возможность воспринимать новую информацию. Все ведь помнят как в школе/институте под конец дня новые знания превращались в кашу, хотя с утра все шло довольно легко? А еще, небольшой тест: представьте себе 10 отдельных людей, со всеми деталями и движениями. Не получится, мы просто не можем держать такой большой контекст одновременно. Обычно предел где-то около 5-6. Но вот само понятие "10 людей", без детализации, уже довольно простое
Учитывая эти особенности, можно уже подумать как правильно применять паттерны
Паттерн должен быть узнаваем.
Это может быть как общеизвестный паттерн из книг, так и локальный на проекте. Главное, чтобы ваш тиммейт знал его и мог легко распознать
Это все банально экономит ресурсы мозга. Встречая что-то новое, тебе нужно адаптироваться и потратить силы чтобы понять это. Если вы видишь что-то знакомое, ты просто достаешь это из памяти как кусочек пазла, и применяешь к ситуации
Например, ты видишь фабрику или прости хоспади синглтон, тебе уже не нужно вчитываться и пытаться понять как это работает. Ты уже знаешь, и тебе уже осталось только понять как это применено. Точно так же это работает и с "локальными" паттернами. Например, в клиент-серверную архитектуру проекта могут быть заложены какие-то проектные особенности. Все на проекте про них знаю и могут использовать
Поэтому стоит избегать своих велосипедов, или еще хуже, небольших отклонений от общепринятых норм. Это часто бывает очень сложно, ведь каждый из нас считает программирование творческой профессией, и каждый желает себя проявить. Да, локально это интересно и полезно лично для тебя: ты попробовал что-то новое и получил удовольствие от вставки частички себя в код. Но на масштабе проекта это плохо, ведь это минус для твоих коллег, которым придется вчитываться в твою кастомщину. Поэтому, в рамках проекта, выгоднее держаться местами хоть и устаревших, но общих и привычных подходов
Это может быть как общеизвестный паттерн из книг, так и локальный на проекте. Главное, чтобы ваш тиммейт знал его и мог легко распознать
Это все банально экономит ресурсы мозга. Встречая что-то новое, тебе нужно адаптироваться и потратить силы чтобы понять это. Если вы видишь что-то знакомое, ты просто достаешь это из памяти как кусочек пазла, и применяешь к ситуации
Например, ты видишь фабрику или прости хоспади синглтон, тебе уже не нужно вчитываться и пытаться понять как это работает. Ты уже знаешь, и тебе уже осталось только понять как это применено. Точно так же это работает и с "локальными" паттернами. Например, в клиент-серверную архитектуру проекта могут быть заложены какие-то проектные особенности. Все на проекте про них знаю и могут использовать
Поэтому стоит избегать своих велосипедов, или еще хуже, небольших отклонений от общепринятых норм. Это часто бывает очень сложно, ведь каждый из нас считает программирование творческой профессией, и каждый желает себя проявить. Да, локально это интересно и полезно лично для тебя: ты попробовал что-то новое и получил удовольствие от вставки частички себя в код. Но на масштабе проекта это плохо, ведь это минус для твоих коллег, которым придется вчитываться в твою кастомщину. Поэтому, в рамках проекта, выгоднее держаться местами хоть и устаревших, но общих и привычных подходов
Архитектура должна быть настолько сложной, насколько это необходимо в данный момент.
Ключевой момент - необходимо в данный момент. Не стоит архитектурить наперед, учитывая все возможные варианты. Как правило невозможно угадать что будет в будущем с проектом, чего именно захочет бизнес. Обычно все идет не так, как ты предполагаешь. Поэтому и архитектурные решения и выбор паттерна стоит делать исходя из совершенно очевидных условий на текущий момент.
Будет ли это тонкий или толстый клиент? Это очевидно сразу. Нужно ли выделить здесь интерфейс? Если тебе прямо сейчас это не нужно и ты не на 100% уверен что в ближайшее время понадобится - тебе не нужно выделять этот интерфейс. Какой смысл от интерфейса, если у него единственная реализация?
Лучше оставить более простой и менее изящный архитектурно код, так как его в случае чего легче рефакторить. Предлагаю смотреть смело правде в глаза - мы постоянно ошибаемся, и всегда есть потребность рефакторить устаревшие решение и под изменения требований.
Что будет проще, разбить монолит или собрать пачку классов в кучу, чтобы снова переразбить? Очевидно первый вариант проще, потому что как правило монолит написан проще. Его легче понять, не так жалко выкинуть и просто сделать лучше
Ключевой момент - необходимо в данный момент. Не стоит архитектурить наперед, учитывая все возможные варианты. Как правило невозможно угадать что будет в будущем с проектом, чего именно захочет бизнес. Обычно все идет не так, как ты предполагаешь. Поэтому и архитектурные решения и выбор паттерна стоит делать исходя из совершенно очевидных условий на текущий момент.
Будет ли это тонкий или толстый клиент? Это очевидно сразу. Нужно ли выделить здесь интерфейс? Если тебе прямо сейчас это не нужно и ты не на 100% уверен что в ближайшее время понадобится - тебе не нужно выделять этот интерфейс. Какой смысл от интерфейса, если у него единственная реализация?
Лучше оставить более простой и менее изящный архитектурно код, так как его в случае чего легче рефакторить. Предлагаю смотреть смело правде в глаза - мы постоянно ошибаемся, и всегда есть потребность рефакторить устаревшие решение и под изменения требований.
Что будет проще, разбить монолит или собрать пачку классов в кучу, чтобы снова переразбить? Очевидно первый вариант проще, потому что как правило монолит написан проще. Его легче понять, не так жалко выкинуть и просто сделать лучше
Кодстайл.
Внезапно да, какого фига мы тут вроде про паттерны? А дело в том, что кодстайл основывается на тех же принципах и служит тем же целям: эффективность работы команды с кодовой базой
Думая о кодстайле, я вспоминаю свой первый месяц работы в social quantum. Я реально целый месяц привыкал ... к отсутствию кодстайла. Это когда в одном файле часть кода написана по одним правилам оформления, а часть по другим. И все это смешано в лютую кашу. Читать такое было просто физически сложно, но я привык. С тех пор имею способность уметь читать почти любой говнокод. Однако, этот месяц было крайне тяжело
Так и что, в чем сила кодстайла, брат? А все в том же, что и у паттернов - это экономит ресурсы мозга и позволяет легче понимать код, и как следствие работать с ним. Легче работать -> больше профита -> больше денег капиталист зарабатывает.
В кодстайле работают все те же принципы паттернов: узнаваемость и повторяемость. В первую очередь визуальная. Ведь код мы буквально читаем глазами, и на это тоже тратится ресурс. Что в таком случае важно в кодстайле?
- единообразие. Это касается не только регистра, отступов и пробелов, но и структуры кода. Если в команде договорились сначала писать публичные поля, а затем приватные, что угодно - главное единообразие везде. Всякие нейминги, camelCase/snake_case, отступы и пробелы легко настраиваются линтерами и автоформатированием в IDE. А вот все остально оформление на плечах разработчиков, и тут стоит стремиться "делать как все"
- визуальная компактность и предсказуемость. Нужно время чтобы водить глазами, особенно если не знаешь куда, это отнимает время. А вот если физически расстояния между точками интереса маленькое, и эти точки предсказуемы, мы делаем это быстро. Этому способствует ограничение ширины кода, а точнее ограничение по номеру столбца в тексте. Обычно больше 120 уже получается слишком широко, и на ноутбучном мониторе все становится совсем плохо, ведь приходится прокручивать
- логическое разделение блоков. Зрительно легче читать алгоритмы, если их логически части буквально графически отделены друг от друга. Банально вставка пустых строк, или комментариев между кусками логической цепочки, уже на уровне зрения создают структуру кода, которую легче воспринять
- цветовая дифференциация. Здесь вроде бы тоже все достаточно очевидно. Наше зрение в первую очередь воспринимает цвет, уже потом форму. Понять что что-то красное сильно проще, чем понять что оно круглое. Но при этом нужно и не переборщить, слишком большой разброс диапазона цветов перегружает нервную систему, и с таким работать сложнее. Благо цветовых схем кода масса и на любой вкус
Внезапно да, какого фига мы тут вроде про паттерны? А дело в том, что кодстайл основывается на тех же принципах и служит тем же целям: эффективность работы команды с кодовой базой
Думая о кодстайле, я вспоминаю свой первый месяц работы в social quantum. Я реально целый месяц привыкал ... к отсутствию кодстайла. Это когда в одном файле часть кода написана по одним правилам оформления, а часть по другим. И все это смешано в лютую кашу. Читать такое было просто физически сложно, но я привык. С тех пор имею способность уметь читать почти любой говнокод. Однако, этот месяц было крайне тяжело
Так и что, в чем сила кодстайла, брат? А все в том же, что и у паттернов - это экономит ресурсы мозга и позволяет легче понимать код, и как следствие работать с ним. Легче работать -> больше профита -> больше денег капиталист зарабатывает.
В кодстайле работают все те же принципы паттернов: узнаваемость и повторяемость. В первую очередь визуальная. Ведь код мы буквально читаем глазами, и на это тоже тратится ресурс. Что в таком случае важно в кодстайле?
- единообразие. Это касается не только регистра, отступов и пробелов, но и структуры кода. Если в команде договорились сначала писать публичные поля, а затем приватные, что угодно - главное единообразие везде. Всякие нейминги, camelCase/snake_case, отступы и пробелы легко настраиваются линтерами и автоформатированием в IDE. А вот все остально оформление на плечах разработчиков, и тут стоит стремиться "делать как все"
- визуальная компактность и предсказуемость. Нужно время чтобы водить глазами, особенно если не знаешь куда, это отнимает время. А вот если физически расстояния между точками интереса маленькое, и эти точки предсказуемы, мы делаем это быстро. Этому способствует ограничение ширины кода, а точнее ограничение по номеру столбца в тексте. Обычно больше 120 уже получается слишком широко, и на ноутбучном мониторе все становится совсем плохо, ведь приходится прокручивать
- логическое разделение блоков. Зрительно легче читать алгоритмы, если их логически части буквально графически отделены друг от друга. Банально вставка пустых строк, или комментариев между кусками логической цепочки, уже на уровне зрения создают структуру кода, которую легче воспринять
- цветовая дифференциация. Здесь вроде бы тоже все достаточно очевидно. Наше зрение в первую очередь воспринимает цвет, уже потом форму. Понять что что-то красное сильно проще, чем понять что оно круглое. Но при этом нужно и не переборщить, слишком большой разброс диапазона цветов перегружает нервную систему, и с таким работать сложнее. Благо цветовых схем кода масса и на любой вкус
Пожалуй, для меня это база про использование паттернов. На мой взгляд это гораздо важнее, чем просто вызубрить кучу паттернов и вывалить их на собеседовании или в код.
Как и везде, понимание сути ведет к более лучшим результатам, чем тупая зубрежка 😉
Как и везде, понимание сути ведет к более лучшим результатам, чем тупая зубрежка 😉
This media is not supported in your browser
VIEW IN TELEGRAM
Из насущных будней - работа поля ассета в редакторе.
Оно отображает данные AssetRef<> и взаимодействует с ним. И тут есть интересная особенность - это instance ассеты. Они хранятся не в отдельном файле, а прямо в сцене, а точнее в конкретном акторе и конкретном компоненте
Работа с такой сущностью требует дополнительного функционала: нужно уметь не только "пробрасывать" ссылку на файл и создавать инстанс, но и сохранять инстанс в файл, сбрасывать его
Так же есть кнопка редактирования, чтобы сразу открыть редактор ассета, если таковой есть. Для этого сделан общий интерфейс окна редактора ассета - IAssetEditorWindow. Достаточно использовать этот интерфейс и кнопка редактирования появится автоматически, т.к. такие окна сами себя регистрируют в системе. В нем уже есть дефолтная панель управления ассетом - сохранить, открыть, создать новый и сбросить
Работа с такой сущностью требует дополнительного функционала: нужно уметь не только "пробрасывать" ссылку на файл и создавать инстанс, но и сохранять инстанс в файл, сбрасывать его
Так же есть кнопка редактирования, чтобы сразу открыть редактор ассета, если таковой есть. Для этого сделан общий интерфейс окна редактора ассета - IAssetEditorWindow. Достаточно использовать этот интерфейс и кнопка редактирования появится автоматически, т.к. такие окна сами себя регистрируют в системе. В нем уже есть дефолтная панель управления ассетом - сохранить, открыть, создать новый и сбросить
o2 dev
Расскажу про свои небольшую разработку внутри playrix, профайл-виджет, показывающий актуальный срез по производительности прямо в игре
——- pew pew ——-
завернул сорцы в отдельный репозиторий. Можно подключить к любому проекту с imgui
https://github.com/zenkovich/imgui_perfmon/tree/main
ps: AI-шка бодро нагенерила коментов в коде 😁
завернул сорцы в отдельный репозиторий. Можно подключить к любому проекту с imgui
https://github.com/zenkovich/imgui_perfmon/tree/main
ps: AI-шка бодро нагенерила коментов в коде 😁
Function<void()> callback;
...
callback += []() { SomethingHappened(); };
callback += []() { SomethingHappenedMoore(); };
Видели такое, мм? )
Те кто писал на C# знают, что это офигенная штука, но в плюсах такого не сделать через +=, придется городить кракозяблы.. Или нет?
Сегодня расскажу про замену std::function в моем движке - o2::Function<>. Он повторяет функционал оригинала из std, но добавляет изрядно синтаксического сахара. Основное - это возможность хранить в коллбеке сразу несколько функций.
А так же немного об оптимизации внутри 😉
Начнем с того, что такое вообще std::function, как оно оборачивает лямду и как это работает.
Вспомним (или узнаем) что лямда - это определенный тип объекта (под каждую лямду отдельный тип), с переопределенной функцией operator(). Собственно когда мы создаем лямду, например вот так:
то мы создаем объект типа этой лямды, в тело которого копируются захваченные переменные var1 и var2, с переопределенной функцией operator(). Этот объект разворачивается примерно в такой код:
Но так происходит не всегда, а только если есть захват переменных. Если нет, то лямда - это просто статичная функция
Собственно, std::function<> заворачивает лямду внутри себя. Он хранит в себе экземпляр объекта типа лямды (my_labmda из псевдокода) и так же имеет перегруженную функцию operator(). Вот псевдокод минимальной реализации std::function
Вспомним (или узнаем) что лямда - это определенный тип объекта (под каждую лямду отдельный тип), с переопределенной функцией operator(). Собственно когда мы создаем лямду, например вот так:
auto myFunc = [var1, var2]() { ... do something ... }
то мы создаем объект типа этой лямды, в тело которого копируются захваченные переменные var1 и var2, с переопределенной функцией operator(). Этот объект разворачивается примерно в такой код:
struct __my_labmda__
{
int var1, var2;
__my_labmda__(int var1, int var2): var1(var1), var2(var2) {}
void operator() { ... do something ... }
};
__my_lambda__ myFunc(var1, var2);
Но так происходит не всегда, а только если есть захват переменных. Если нет, то лямда - это просто статичная функция
Собственно, std::function<> заворачивает лямду внутри себя. Он хранит в себе экземпляр объекта типа лямды (my_labmda из псевдокода) и так же имеет перегруженную функцию operator(). Вот псевдокод минимальной реализации std::function
template<typename _lambda_type, typename _res_type, typename ... _args>
struct function
{
_lambda_type lambda; // Храним объект лямды
function(_lambda_type&& lambda): lambda(std::forward<_lambda_type>(lambda)) {} // Конструируемся из лямды
_res_type operator(_args ... args) { return lambda(args ...); } // Вызов лямды
};
Из этого псевдокода уже видно, что function может хранить только одну lambda. Вторую и последюущие добавить так просто не получится, тк function уже специализирован под конкретный тип лямды - _lambda_type
Выход простой - завернуть _lambda_type lambda в промежуточный объект с интерфейсом:
Который уже использовать для различных типов лямд - SharedLamda. Так же можно под этот интерфейс завернуть и другие типы функций:
- статичная функция - FunctionPtr
- функция класса - ObjFunctionPtr
Далее function превращается в контейнер объектов от интерфейса IFunction:
Этот всевдокод уже показывает как нам сохранить несколько функций в одной. В реальном Function<> богатый api для работы с разными фнукциями и лямдами сразу. Они конструируют нужную реализацию IFunction внутри и добавляют в список
Но что насчет перфоманса? Тут и аллокации, и поинтеры? Моя первая реализация и правда была настолько простой, как в псевдокоде. Но это давало ощутимые просадки, ведь даже на передачу одной лямды требуется работа с вектором, а это и лишняя память, и аллокации...
Выход простой - завернуть _lambda_type lambda в промежуточный объект с интерфейсом:
template<typename _res_type, typename ... _args>
class IFunction<_res_type(_args ...)>
{
public:
virtual _res_type Invoke(_args ... args) const = 0;
};
Который уже использовать для различных типов лямд - SharedLamda. Так же можно под этот интерфейс завернуть и другие типы функций:
- статичная функция - FunctionPtr
- функция класса - ObjFunctionPtr
Далее function превращается в контейнер объектов от интерфейса IFunction:
template<typename _lambda_type, typename _res_type, typename ... _args>
struct function
{
std::vector<IFunction<_res_type(_args ...)>*> functions; // Храним объекты функций
// Добавляем функцию
function& operator+(IFunction<_res_type(_args ...)>* func)
{
functions.push_back(func);
}
// Вызываем все функции, возвращаемое значение берем от последней
_res_type operator(_args ... args)
{
if (functions.size() == 0)
return _res_type();
for (int i = 0; i < functions.size() - 1; i++)
functions[i]->Invoke(args ...);
return functions.back()->Invoke(args ...);
}
};
Этот всевдокод уже показывает как нам сохранить несколько функций в одной. В реальном Function<> богатый api для работы с разными фнукциями и лямдами сразу. Они конструируют нужную реализацию IFunction внутри и добавляют в список
Но что насчет перфоманса? Тут и аллокации, и поинтеры? Моя первая реализация и правда была настолько простой, как в псевдокоде. Но это давало ощутимые просадки, ведь даже на передачу одной лямды требуется работа с вектором, а это и лишняя память, и аллокации...
Здесь я применил довольно простую оптимизацию, наподобие small string optimization. Суть ее простая - если один объект по размеру меньше или равен контейнеру, в который он помещается, то он хранится прямо в области памяти контейнера
Чуть подробнее. Для этого используется Union - это такая штука позволяющая в одном участке памяти как бы хранить несколько типов данных сразу. Все сразу использовать нельзя, но эту память можно интерпретировать как один из этих типов. Например:
Прям так как в примере, естественно, делать нельзя - нужно знать что именно хранится в d. Ведь под все типы данных у нас единая память. Компилятор считает наибольший размер, выравние и удобно заворачивает доступ к конкретному типу
Для более простой работы используется современный std::variant, однако union'ы нам нужны для понимания оптимизации
Ведь в этом union хранится сразу два варианта хранения функций внутри: одна функция или множество
Чуть подробнее. Для этого используется Union - это такая штука позволяющая в одном участке памяти как бы хранить несколько типов данных сразу. Все сразу использовать нельзя, но эту память можно интерпретировать как один из этих типов. Например:
union data
{
float number;
std::string string;
bool flag;
};
data d = ...;
d.number = 5; // запись данных из области памяти d в виде float
d.string = "hello"' // запись данных из области памяти d в виде строки
d.flag = true; // запись данных из области памяти d в виде boolean
Прям так как в примере, естественно, делать нельзя - нужно знать что именно хранится в d. Ведь под все типы данных у нас единая память. Компилятор считает наибольший размер, выравние и удобно заворачивает доступ к конкретному типу
Для более простой работы используется современный std::variant, однако union'ы нам нужны для понимания оптимизации
Ведь в этом union хранится сразу два варианта хранения функций внутри: одна функция или множество
Вот вырезка из кода, в каком виде это хранится:
Рассмотрим по порядку:
-
-
- кусок памяти под саму IFunction -
- указатель на деструктор этой функции. Он необходим для корректного освобождения хранимой функции. Ведь в ней могут быть захвачены переменные, которые необходимо корректно освободить и тп
-
Когда Function<> конструируется из одной IFunction<>, например небольшой лямды или указателя на функцию класса, то включается оптимизация и эта функция без аллокации сохраняется прямо в functionData[capacity].
Если она больше, или функций больше одной, то переключаемся на вариант с std::vector<>
Это работает довольно хорошо, потому что в большинстве случаев мы все-таки в Function<> храним только одну функцию. Чуть затратнее по памяти, однако косты на аллокации срезаются. У меня разница в производительности была заметна на глаз - этап загрузки редактора с кучей коллбеков ускорился значительно. Лишний раз подтвердило что аллокации - зло
struct TypeData
{
Byte padding[payloadSize];
DataType type;
};
struct OneFunctionData
{
static constexpr UInt capacity = payloadSize - sizeof(void*);
Byte functionData[capacity];
void(*destructor)(IFunction<_res_type(_args ...)>*) = nullptr;
};
union Data
{
std::vector<IFunction<_res_type(_args ...)>*> functions;
OneFunctionData oneFunctionData;
TypeData typeData;
};
Рассмотрим по порядку:
-
TypeData
- контейнер типа хранимых данных. Типа хранится позади padding[payloadSize]-
OneFunctionData
- контейнер для одной функции. Внутри:- кусок памяти под саму IFunction -
Byte functionData[capacity]
, - указатель на деструктор этой функции. Он необходим для корректного освобождения хранимой функции. Ведь в ней могут быть захвачены переменные, которые необходимо корректно освободить и тп
-
std::vector<IFunction<_res_type(_args ...)>*> functions
- собственно вектор с объектами функцийКогда Function<> конструируется из одной IFunction<>, например небольшой лямды или указателя на функцию класса, то включается оптимизация и эта функция без аллокации сохраняется прямо в functionData[capacity].
Если она больше, или функций больше одной, то переключаемся на вариант с std::vector<>
Это работает довольно хорошо, потому что в большинстве случаев мы все-таки в Function<> храним только одну функцию. Чуть затратнее по памяти, однако косты на аллокации срезаются. У меня разница в производительности была заметна на глаз - этап загрузки редактора с кучей коллбеков ускорился значительно. Лишний раз подтвердило что аллокации - зло
И, напоследок, небольшой бонус. Как это вообще так определяется шаблонный класс, умеющий принимает в себя аргументы шаблонов в специфичном виде? ))
Просто так объявить такой шаблон не получится... Все дело в магии forward'а этого класса. Если объявить его в таком простом виде, то затем можно сказать компилятору принимать шаблоны в этом специфичном виде
Полный исходник можно посмотреть здесь: https://github.com/o2-engine/o2/blob/master/Framework/Sources/o2/Utils/Function/Function.h
Function<_res_type(_args ...)>
Просто так объявить такой шаблон не получится... Все дело в магии forward'а этого класса. Если объявить его в таком простом виде, то затем можно сказать компилятору принимать шаблоны в этом специфичном виде
template <typename UnusedType>
class IFunction;
template<typename _res_type, typename ... _args>
class IFunction<_res_type(_args ...)>
{ ... };
Полный исходник можно посмотреть здесь: https://github.com/o2-engine/o2/blob/master/Framework/Sources/o2/Utils/Function/Function.h