Динамические типы данных добавляют гибкости нашему коду, они позволяют использовать скриптовые языки программирования в среде dotnet, позволяют хитрым образом избавится от написания лишнего кода при работы с объектами через рефлексию и вобще мегакруты. Но с большой силой приходит большая ответственность, а за лишнее удобство приходится платить дополнительную цену. И цена здесь - это скорость работы. Об этом должен знать каждый разработчик платформы дотнет. Но вот из-за чего происходит замедление, и насколько сильно просаживается скорость работы это уже вопросы со звёздочкой. И если вам так-же интересно знать ответы на эти вопросы, прошу проследовать под кат.
От переводчика: в этой статье я скомпоновал пару прекрассных ответов со стековерфлоу. По работе компилятора при взаимодействии с dynamic читать ответ Eric Lippert здесь.
Почему dynamic медленный?
При обращении к динамическим типам компилятор добавляет код, который добавляет создание объекта сайта (места) динамического вызова, внутри которого и происходит операция. К примеру, у вас есть вот такой кусок кода:
class Program
{
class MyClass
{
public int GetY()
{
return 42;
}
}
static void Main(string[] args)
{
dynamic y = new MyClass();
var result = y.GetY();
}
}
То компилятор выдаст код близкий к этому. (Реально код чуть сложнее, но нам этого хватит для демонстративных целей)
internal class Program
{
private static void Main(string[] args)
{
if (a > b) d = c;
object y = (object) new Program.MyClass();
if (Program.<>o__1.<>p__0 == null)
{
Program.<>o__1.<>p__0 = CallSite<Func<CallSite, object, object>>.Create(
Binder.InvokeMember(
CSharpBinderFlags.None,
"GetY",
(IEnumerable<Type>) null,
typeof (Program),
(IEnumerable<CSharpArgumentInfo>) new CSharpArgumentInfo[]
{
CSharpArgumentInfo.Create(
CSharpArgumentInfoFlags.None,
(string) null
)
})
);
}
object result = Program.<>o__1.<>p__0.Target((CallSite) Program.<>o__1.<>p__0, y);
}
Давайте немного разберём, что же здесь проимходит? Во первых создаём обект-сайт вызова и кэшируем его, для использования в дальнейшем. Вызывающий объект-сайт будет существовать постоянно после того, как вы первый разобратитесь к нему. Объект-сайт вызова - это такой объект, в задачей которого является динамическое обращение к методу GetY
.
После создания объекта сайта вызова произойдёт обычный вызов метода ?
Конечно нет! Этот объект представляет из себя времени run-time динамического языка (DLR). DLR как-бы дмает: «Хм, сейчас происходит обращение к динамическому методу foo вот такого объекта. Каким образом это должно произойти? Нет. Тогда я лучше узнаю.
После чего DLR просмотривает объект d1, чтобы разобраться, чем эта штука является. Это может быть COM-объект или объект Iron Python, или объект Iron Ruby, или объект IE DOM. Если это не кто-либо из них, тогда получается это не что иное как объект C#.
И вот тут снова дело в свои руки берёт компилятор. Здесь не нужен лексере или синтаксический анализатор, поэтому DLR вызывает особую версию компилятора C#, которая при помощи анализатора метаданных и семантического анализатора выражений возвращает деревья выражений, а не обычный IL.
Анализатор метаданных используя рефлексию определяет тип объекта d1, после чего передаст его семантическому анализатору, чтобы определить, что произойдёт, если у такого объекта вызываеть метод Foo. Анализатор перегрузки методов показывает эту информацию, а затем компилятор создаст дерево выражений - точно так же, как если бы вы вызвали этот метод Foo при помощи дерева выражений внутри lambda выражения.
На следующем этапе компилятор C# передаст, созданное дерево выражений назад в DLR дополнив политикой кэширования. Обычно эта политика представляет из себя правило - во второй раз, когда вы увидите объект такого типа, используйте уже созданное дерево выражений, а не обращаться ко мне. После чего DLR осуществляет компиляцию дерева выражений при помощи в свою очередь компиляторп expression-tree-to-IL и возвращает блок IL как делегат.
DLR кэширует созданный делегат в кеше, связанном с объектом сайта вызова.
Затем он вызывает делегат, и происходит вызов Foo. В следующие разы, когда вы вызываете в M, у нас уже есть возможность обратится к сайт-обьекту. DLR снова запросит объект, и если объект такого-же типа, что и в прошлый раз, он извлечёт делегат из кэша и вызовет его. Но в случае если прийдёт объект другого типа, то кеш промахётся, и весь процесс придётся пройти заново.
Такое будет происходить для любого выражения, которое каким-либо образом связано с динамическими переменными. Так, например, если у вас есть:
int x = d1.Foo() + d2;
Здесь будет ТРИ динамических вызова и соответственно сайта. Один для вызова метода Foo, один для добавления и один для преобразования из динамического типа в int. Каждый из них имеет свой объект сайт и для каждого из них потребуется анализ времени выполнения.
Ну как? Уже не кажется такой хорошей идеей завести больше обращений к dynamic
? С другой стороны представте как много кода не пришлось писать вам. А ведь лучший код, это не написанный код!
На сколько же плохо для производительности лишнее обращение к dynamic?
Судя по бэнчмарку использование dynamic просаживает скорость в 6.7 раз по сравнению с статически типизированным кодом. Насколько это подходит для вашего случая, можно посудить взглянув на сравнительную таблицу и прикинув альтернативные решения опирающиеся на другие типы вызова:
Тип вызова | Относительное время вызова |
---|---|
Обычный метод | 1 |
Метод расширения | 1,19 |
Виртуальный интерфейсный метод | 1,46 |
Обобщённый метод | 1,54 |
DynamicMethod.Emit | 2,07 |
MethodInfo.CreateDelegate | 2,13 |
Visitor Accept/Visit | 2,64 |
Linq выражение | 5,55 |
Ключевое слово dynamic | 6,70 |
MethodInfo Invoke | 102,96 |
Если вдруг интересны детали исследования просадки производительности и вы хотите перепроверить коэфициенты вы можете найти исследование здесь.
comments powered by Disqus