Комментарии 11
быстро как и обычный, без накладных расходов на использование метаданных, как это обычно бывает при использовании reflection.
поясните плиз подробнее! каков примерно выигрыш в скорости в % ?
Я не назову вам конкретных цифр, т.к. они очень сильно зависят от того, что вы пытаетесь оптимизировать, насколько часто этот код вызывается и какую долю занимает в остальном коде. Обычно разница такая же, как если сравнивать универсальный код для решения нескольких задач и код, который написан под конкретную задачу.
Например, если это код какого-то маппера, который копирует данные из одного объекта в другой, то реализация через reflection может быть на порядок или на два медленнее.
Тут приводятся некоторые цифры по поводу использования reflection: https://stackoverflow.com/questions/25458/how-costly-is-net-reflection
Естественно, нет никакого смысла писать динамический метод, чтобы вызвать его один раз на старте приложения.
Правильно ли я понимаю, что после JIT компиляции структура исполнения алгоритма остаётся в виде стековой машины? Это какая-то эмуляция или идёт прямая трансляция на команды конкретной машины для прямой работы со стеком (ну если они есть, конечно)? То есть регистры процессора так и остаются незадействованные (или исп. только для служебных целей) или JIT-компилятор может что-то оптимизировать и переносить в регистры и применять соответствующие команды? Особенно как обрабатываются intrinsic команды (поддержка короых не так давно появилась в .NET)?
Где размещаются аргументы функции и локальные переменные до того как они переносятся на стек или обратно? А как идёт доступ к переменным/аргументам более высокого уровня (из локальных функций или доступ к this ссылки владельца функции)? Сам этот перенос - это копирование значения из одной ячейки памяти в другую (т.е. в процессе обработки переменных/аргументов они постоянно копируются)? А как обрабатываются переменные структурных типов (структуры, кортежи.... отдельно про классы)?
Правильно ли я понимаю, что после JIT компиляции структура исполнения алгоритма остаётся в виде стековой машины?
Нет, всё компилируется в машинный код, как с обычным .NET кодом. Фактически динамические методы хранятся в динамической сборке, которая практически ничем не отличается от обычных dll сборок к которым все привыкли.
Разница только в том, что обычный код вы компилируете из C# в IL, а IL потом через JIT компилируется в машинный код. А с динамическими методами (а также с компилируемыми Expresson Trees и динамическими сборками) вы в runtime генерируете сразу IL, который для выполнения через JIT компилируется в машинный код.
За исключением, может быть, того, что динамические методы, если на них не остаётся ссылок в коде потом собираются GC.
Особенно как обрабатываются intrinsic команды (поддержка короых не так давно появилась в .NET)?
Всё что умеет оптимизировать JIT компилятор, всё будет оптимизироваться и в динамических методах.
Где размещаются аргументы функции и локальные переменные до того как они переносятся на стек или обратно?
Я не уверен, что понял вопрос. Они размещаются там же где и аргументы и локальные переменные в обычном коде.
А как идёт доступ к переменным/аргументам более высокого уровня (из локальных функций или доступ к this ссылки владельца функции)?
Локальные функции, которые обращаются к переменным из окружающего контекста - это замыкания. Для замыканий C# создаёт специальный объект, где хранит все эти переменные.
Т.е. нужно научиться в runtime генерировать точно такой же код, который сгенерировал бы компилятор C#. Это не самая тривиальная задача, но это можно сделать.
По поводу this. Я постараюсь описать это в следующей статье. При создании делегата через CreateDelegate можно указать параметр target, который хранит объект, экземпляром которого будет являться метод. В этом случае все параметры метода "сдвигаются" на единицу, а this становится самым первым параметром и его значение можно получить через ldarg.0.
Сам этот перенос - это копирование значения из одной ячейки памяти в другую (т.е. в процессе обработки переменных/аргументов они постоянно копируются)?
Нет, не копируются. Стековая машина - это абстракция, которая упрощает понимание. Любой код .NET работает через стековую машину. Потом всё это оптимизируется при компиляции в машинный код.
А как обрабатываются переменные структурных типов (структуры, кортежи.... отдельно про классы)?
Это хороший вопрос. Я постараюсь ответить на него в следующей статье =)
Разница только в том, что обычный код вы компилируете из C# в IL, а IL потом через JIT компилируется в машинный код. А с динамическими методами (а также с компилируемыми Expresson Trees и динамическими сборками) вы в runtime генерируете сразу IL, который для выполнения через JIT компилируется в машинный код.
Стековая машина - это абстракция, которая упрощает понимание. Любой код .NET работает через стековую машину. Потом всё это оптимизируется при компиляции в машинный код.
Я просто не знаю какой машинной код компилирует JIT - это может быть и машинный код реализующий сегменты стековой машины. Или машинной код, оптимизирующий работы посредством регистров процессора? То есть JIT компилирует алгоритм для стековой машины в алгоритм для регистровой машины (конкретной платформы в лице исполнителя CPU)?
Насколько я понимаю, асинхронный код генерировать таким образом очень неудобно, expressions тоже не не очень для асинхронщины, стоит смотреть в сторону source generators?
Да, асинхронные методы сложно будет сгенерировать динамически, т.к. нужно сгенерировать код стейт-машины. Я бы в этом случае смотрел в сторону того, чтобы код с async-await написать на C# с минимальным количеством логики, а из него вызывать синхронные динамические методы.
По поводу source generators. Я бы сказал, что у них по отношению к динамическим методам разные области применения.
Source generators подходят для генерации кода на основе исходников. Например вы разметили код какими-то атрибутами и по ним сгенерировали код на этапе компиляции.
Динамические методы больше подходят для ситуаций, когда вы на этапе компиляции не знаете, что нужно сгенерировать. Например, у вас в БД лежит какой-нибудь шаблон емейла или формула, которая вводится пользователями в интерфейсе. И этот шаблон или формула вызывается большое количество раз. Для оптимизации тут можно применить динамический метод.
Есть же ещё .NET Compilation Platform (а именно часть Roslyn влице .NET Scripting)- там код можно писать прям на C# - затем компилировать его в сборку (она правда, будет с изолированным контекстом, применяющим тонкую proxy-послойку - но всё в одном домене приложения так что все ссылки на объекты в куче будут валидные, только стеки разные) - и при желании - через рефлексию выдернуть даже IL байткод (и уже создать динамический метод в текущем контексте на основе этого байткода - вот только встроить его в существующую стейтмашину навряд ли просто удастся); вот нет стандартных путей превращения байткода в поток IL op-кодов - для упрощения модификации. Но тут можно поискать библиотеку для такой конвертации (но я не нашёл, хотя все программы-рефлеторы это умеют делать - и это не так уж сложно)
Другой подход - а кто мешает обернуть динамический метод асинхронную оболочку, передавая метод в качестве делегата (хотя этот и несколько увеличит затраты на вызов такого метода, зато оболочка сама будет встраиваться в стейтмашину)
По поводу генераторов исходного кода C# (опять же Roslyn) - всё тоже не просто - к сожалению механизм позволяет генерировать только новый код (хотя есть техника замены старого кода на новый - но безвозвратно - т.е. прямо в самих исходных исходниках - прошу прощения за тавтологию) - лично я не знаю техники, позволяющий исключить исходный исходный код из итоговой компиляции, заменив его на новый, так чтобы он так и остался в исходниках (вероятно его можно сразу настраивать не компилируемым со всеми вытекающими - автоматически анализируемым он тоже не будет). Тем самым сильно затрудняется процесс встраивания сгенерированного кода существующий!
Но и не надо забывать, что генераторы исходного кода статические. Всё-таки потребность в динамических методах больше, скорее, в необходимости генерировать их на основе рантайм состояния.
И тут может быть интересная и гибридная техника: когда основной код генерируется на .NET Scripting (на ЯП C# - потому что это проще, или в виде синтаксического дерева - немного сложнее, чем текстом - зато красивее, но в данном случае бессмысленно, т.к. для .NET Scripting нужен именно текст алгоритма, хотя выражения синтакс.дерева легко превращаются в текст), затем его вызов оборачивается динамическим методом (кстати библиотеки такие уже есть для .NET Scripting), а уже динамический метод (или делегат) далее используется в основном коде, например в качестве подключаемого обработчика события, или вот далее встраивается в основную стейтмашину, или используется в качестве какого-либо ремоут взаимодействия!
Динамические методы в .NET