Главная страница   /   5.5. Легковесная генерация кода при помощи динамических методов (Метапрограммирование в .NET

Метапрограммирование в .NET

Метапрограммирование в .NET

Кевин Хазард

5.5. Легковесная генерация кода при помощи динамических методов

Другая технология создания кода на основе IL использует класс DynamicMethod. Этот класс генерирует для вас методы во время выполнения. Давайте рассмотрим причины того, что это использовать лучше, чем Reflection.Emit, а затем мы покажем вам, как вы могли бы это использовать, на примере ToString() из раздела 2.4.2.

Когда создавать сборку - это чересчур

Есть два недостатка Reflection.Emit, когда речь идет о генерации кода во время выполнения. Первая проблема заключается в сложности, или тяжести подхода с Reflection.Emit. Если все, что вам нужно, это создать небольшой фрагмент кода, вы должны создать динамическую сборку, модуль и тип, чтобы создать метод. Для большей части динамического кода, который вы видели, нужно всего лишь создать метод, что делает этот подход несколько громоздким. Конечно, бывают случаи, когда вам нужно создать полную сборку (или хотя бы тип, как в случае создания динамических прокси), но, в остальном, подход с динамическим методом это то, что нужно.

Другая проблема – это вопрос нехватки памяти. При создании динамической сборки, эта сборка загружается в домен, из которого она была создана, как правило, по умолчанию это AppDomain. Проблема в том, что вы не можете напрямую выгружать сборку; единственный способ выгрузить сборку из памяти – это вызвать Unload() для AppDomain, в котором находится сборка. Это, вероятно, то, чего вы не захотите делать для AppDomain по умолчанию! Вы можете обойти это путем создания динамических сборок в других AppDomain, которые вы создаете, но это не тривиальная задача. По мере создания все большего числа динамических сборок, память будет заполняться и заполняться, и если вы не будете осторожны, вы столкнетесь с проблемами памяти.

Примечание

С 4.0 можно отметить динамическую сборку как выгружаемую с помощью значения AssemblyBuilderAccess.RunAndCollect при создании сборки. Это позволит выгрузить сборку, если это необходимо, что является большим преимуществом с точки зрения давления на память. Это не является надежным решением, так как вы должны придерживаться набора ограничений, которые связаны с памятью, хотя эти ограничения, вероятно, не повлияют на большинство сценариев, с которыми вы будете работать в качестве разработчика. Посетите http://mng.bz/mK5M для получения дополнительной информации об этих ограничениях.

С DynamicMethod вам не нужен весь API для создания динамического кода. Все, что у вас есть, как только DynamicMethod будет сделан, это метод, который можно вызвать, как и любой другой метод (более или менее). И хорошая вещь с DynamicMethod состоит в том, что мусор собирается по умолчанию. Поэтому, как только вы используете DynamicMethod, вам не придется беспокоиться о засорении памяти. GC с радостью избавиться от него, как только он определит, что это больше не используется.

Теперь, когда вы знаете, в чем заключаются основные различия между Reflection.Emit и DynamicMethod, давайте посмотрим, как можно использовать этот класс для создания метода во время выполнения.

Создание методов прокладки

Давайте вернемся к примеру ToString(), который вы видели в разделе 2.4.2. Вместо того чтобы использовать рефлексию, давайте сгенерируем метод, который создаст описательную строку во время выполнения. Помните, что цель заключается в том, чтобы взять все открытые, читаемые свойства на уровне экземпляра для объекта и объединить их вместе. Как и в случае с классом CustomerReflection, давайте создадим класс CustomerDynamicMethod, который откладывает реализацию ToString() для вспомогательного метода расширения:

public sealed class CustomerDynamicMethod : Customer
{
	public override string ToString()
	{
		return this.ToStringDynamicMethod();
	}
}

Реализация ToStringDynamicMethod() похожа на код в листинге 5-6. Если DynamicMethod не существует для заданного типа, оно создается и добавляется в поле Dictionary<Type, Delegate>. Создание метода находится в методе, называемом CreateToStringViaDynamicMethod(): его реализация показана в следующем листинге. Как вы можете видеть, вы просто создаете свой динамический метод, добавляете коды операций и создаете делегат.

Листинг 5-11: Создание динамического кода при помощи DynamicMethod
private static Func<T, string> CreateToStringViaDynamicMethod<T>()
{
	var target = typeof(T);
	var toString = new DynamicMethod(
		"ToString" + target.GetHashCode().ToString(),
		typeof(string), new Type[] { target });
	toString.GetILGenerator().Generate(target);
	return (Func<T, string>)toString.CreateDelegate(
		typeof(Func<T, string>));
}

Конструктор DynamicMethod похож на метод DefineMethod() для TypeBuilder. Вы предоставляете имя метода, тип возвращаемого значения и типы параметров (если они есть). Как только у вас появляется DynamicMethod, вы получаете ILGenerator через GetILGenerator(). И угадайте что? Вы можете повторно использовать метод расширения Generate() из фрагмента кода в подразделе "Создание типа" раздела 5.4.1, потому что это точно такая же генерация кодов операций. Последним шагом является превращение метода в делегат, и тогда вы закончите.

Как вы можете видеть, когда все, что вам нужно, это создать метод, подход с DynamicMethod лучше, чем использование Reflection.Emit. Вы все еще должны создавать коды операций, но вы не должны создавать строящие классы, чтобы попасть туда.

Использование кэширования для повышения производительности

В обеих реализациях, с Reflection.Emit и DynamicMethod, была использована коллекция (в частности, словарь) для хранения динамических артефактов кода, как только они были созданы. Это чрезвычайно важная техника для использования при создании динамического кода, если таковое возможно. Как только вы создали необходимый код ToString(), вам не нужно его воссоздавать, так как определение типа не изменится за время жизни приложения. На самом деле, если вы не кэшируете динамический код, производительность будет сильно страдать. Рисунок 5-7 представляет собой график, который показывает относительную производительность всех техник, которые вы видели в этой главе и в главе 2 для реализации ToString().

Рисунок 5-7: Относительная производительность техник динамического кодирования. Как и ожидалось, рефлексия является самой медленной.

Производительность среди всех пяти подходов относительна. Reflection.Emit является самым быстрым из всех (даже быстрее, чем подход с жестким кодированием!). Рефлексия более чем в 10 раз хуже, чем Reflection.Emit. Но если отключить кэширование, удалив коллекцию, все становится очень, очень плохо для динамического кода. Рисунок 5-8 показывает настроенные значения величин без кэширования, и до боли ясно, что вы должны использовать кэширование, когда создаете динамический код.

Рисунок 5-8: Удаление кэширования из динамического кода. Без кэширование ваше решение может иметь проблемы с производительностью, поэтому кэшируйте динамические результаты, где это только возможно.

Без кэша Reflection.Emit становится в 4000 раз хуже, чем подход с жестким кодированием. Если добавить информацию об отладке, то производительность будет еще хуже. Идея ясна: кэшируйте результаты динамического кода, где это только возможно.

Недостатки DynamicMethod

В DynamicMethod есть недостатки: с отладкой и верификацией. С DynamicMethod нет никакого способа генерировать отладочную информацию на лету, как вы можете это сделать с классами Reflection.Emit. Таким образом, вы не можете войти в код, который вы генерируете в DynamicMethod. Другая проблема состоит в проверке кода. В разделе 5.4.3 упоминается инструмент под названием peverify.exe, который гарантирует, что код, который вы создаете при помощи Reflection.Emit, правильный. Но peverify работает только со сборками, а не конкретными методами. Это означает, что у вас нет способа убедиться, что код в динамическом методе поддается верификации.

Есть один способ обойти это – использовать инъекцию зависимостей. Вот как это работает. Вы создаете интерфейс, у которого будет две реализации: та, которая использует DynamicMethod, и та, которая использует Reflection.Emit. Интерфейс определяет метод, который будет возвращать Func или Action, который динамически создается конкретным классом. Во время отладки и тестирования вы можете использовать версию, которая пользуется Reflection.Emit, так что вы можете проверить код и отладить его с помощью техник, описанных в этой главе. Как только вы будете уверены, что все работает, как и ожидалось, вы можете перейти на реализацию с DynamicMethod. В любом случае, вам не важно, как был создан динамический код.

Давайте использовать реальный код, чтобы показать такой подход. Во-первых, вот интерфейс:

public interface IToStringBuilder
{
	string ToString<T>(T target);
}

Теперь создайте две реализации этого интерфейса. Следующий листинг показывает два конкретных класса. Они используют примеры из разделов 5.4.1 и 5.5.2.

Листинг 5-12: Конкретные реализации IToStringBuilder
public sealed class ToStringDynamicMethodBuilder
	: IToStringBuilder
{
	public string ToString<T>(T target)
	{
		return target.ToStringDynamicMethod();
	}
}
public sealed class ToStringReflectionEmitBuilder
	: IToStringBuilder
{
	public string ToString<T>(T target)
	{
		return target.ToStringReflectionEmit();
	}
}

Вот где гибкость этого подхода становится очевидной. Вы создаете версию Customer, которая принимает ссылку на IToStringBuilder в конструкции:

public sealed class CustomerDependencyInjected
	: Customer
{
	public CustomerDependencyInjected(IToStringBuilder builder)
		: base()
	{
		this.Builder = builder;
	}
	public override string ToString()
	{
		return this.Builder.ToString(this);
	}
		private IToStringBuilder Builder { get; set; }
}

Метод ToString() получает возвращаемое значение от builder, внедренного в него. Это дает вам простой способ поменять ваши два подхода. При создании такой версии customer, вы указываете, какой builder для динамического кода вы хотите использовать:

new CustomerDependencyInjected(
	new ToStringDynamicMethodBuilder())

Если вы столкнулись с проблемами генерации кода, вы замените класс на тот, который вы можете отладить и проверить:

new CustomerDependencyInjected(
	new ToStringReflectionEmitBuilder())

Примечание

Есть один способ узнать, что представляет собой IL, который вы создаете в DynamicMethod, через визуализатор отладки. Посетите http://mng.bz/j4s9, чтобы узнать, как это сделать. Также можно отладить DynamicMethod через WinDBG, но это не тривиальное решение. Все подробности о том, как это сделать, можно найти на http://mng.bz/1s3m.