Оптимизируем свой собственный Inversion of Control Framework
- modified:
- reading: 3 minutes
Я люблю изобретать велосипеды для вещей, которые я очень часто использую. Свой собственный фреймворк Inversion Of Control – это как раз тот случай. Я уже как-то показывал очень простую реализацию Inversion Of Framework, которую я использую во всяческих своих приложениях Реализуем сами простой IoC контейнер (и он уже даже вырос до более интересного проекта OutcoldSolutions.Framework).
В общем, как-то я напоролся на вот это сравнение IoC Container Benchmark - Performance comparison, в котором сравнивалась производительность различных контейнеров. И тут я задался тем же вопросом о том, насколько производительный мой фреймворк. Вот результат (я уменьшил количество итераций, так что не стоит сравнивать с оригинальной статьей):
В этом списке "Outcold" – это то, что я использую сейчас (ранняя версия на github). "IoC E" – это мой вариант из предыдущей статьи, построенный на Expressions (смотри комментарий), "IoC A" – это первоначальный вариант из предыдущей статьи, построенный на обычном Activator.CreateInstance. "LightInject" – один из самых шустрых фреймворков, "Unity" – это то, что я раньше использовал везде. Самое смешное, что после той прошлой статьи – мне как сказали, что Activator медленный, и стоит использовать Expressions – я так и сделал и стал использовать реализацию "IoC E" везде (а как оказалось - это самый тормозной IoC фрейморк, который вы только можете представить). Все дело в том, что каждый раз, когда вызывался метод CreateInstance, я каждый раз компилировал при помощи Expressions код, который должен быть создавать объекты:
То есть, первая оптимизация должна была быть очень простая. Нужно было просто переписать Expressions таким образом, чтобы можно было бы их сохранять и использовать каждый раз создавая объекты одного и того же типа. Как вы видите в примере, в конструктор я передаю константы, поэтому данные Expression не получится сохранить для повторного использования, если значения для аргументов конструктора будут другие. То есть, нужно из этого создать Expression функцию, которая могла бы принимать параметры для конструктора. В результате я переписал Expression так:
Теперь я сохраняю скомпилированный Delegate, возвращаемый последней строкой. Проблема теперь в том, что единственный способ, который я знал, как вызвать этот делегат – это через DynamicInvoke (которая достаточно медленная):
В любом случае, разница с тем, как было без хранения Expression, значительная (но как мы видим, до сих пор хуже реализации с Activator):
Я не сдался, проанализировав код еще раз, запустив наш профилировщик от Visual Studio, я нашел слабое место – это DynamicInvoke. Мне нужно было найти способ, при помощи которого я мог бы конвертировать Delegate в метод. Мне пришла идея написания такого Expression:
Здесь я на входе всегда получаю массив параметров (object[]), затем я каждый элемент массива я последовательно сопоставляю с параметрами конструктора. Результат такой:
Теперь быстрее, чем Activator, но все же чуток медленнее, чем LightInject. Предполагаю, что он шустрее в силу того, что они использовали System.Reflection.Emit. Говорю в прошедшем времени, так как вижу, что последняя версия библиотеки LightInject тоже использует Expressions.
На этом я и остановился. Не самая быстрая версия у меня, но выше середины.