Производительность скомпилированного выражения для делегата
Я генерирую дерево выражений, которое отображает свойства из исходного объекта в целевой объект, который затем компилируется в Func<TSource, TDestination, TDestination>
и выполнен.
Это представление отладки полученного LambdaExpression
:
.Lambda #Lambda1<System.Func`3[MemberMapper.Benchmarks.Program+ComplexSourceType,MemberMapper.Benchmarks.Program+ComplexDestinationType,MemberMapper.Benchmarks.Program+ComplexDestinationType]>(
MemberMapper.Benchmarks.Program+ComplexSourceType $right,
MemberMapper.Benchmarks.Program+ComplexDestinationType $left) {
.Block(
MemberMapper.Benchmarks.Program+NestedSourceType $Complex$955332131,
MemberMapper.Benchmarks.Program+NestedDestinationType $Complex$2105709326) {
$left.ID = $right.ID;
$Complex$955332131 = $right.Complex;
$Complex$2105709326 = .New MemberMapper.Benchmarks.Program+NestedDestinationType();
$Complex$2105709326.ID = $Complex$955332131.ID;
$Complex$2105709326.Name = $Complex$955332131.Name;
$left.Complex = $Complex$2105709326;
$left
}
}
Вычистить это будет:
(left, right) =>
{
left.ID = right.ID;
var complexSource = right.Complex;
var complexDestination = new NestedDestinationType();
complexDestination.ID = complexSource.ID;
complexDestination.Name = complexSource.Name;
left.Complex = complexDestination;
return left;
}
Это код, который отображает свойства этих типов:
public class NestedSourceType
{
public int ID { get; set; }
public string Name { get; set; }
}
public class ComplexSourceType
{
public int ID { get; set; }
public NestedSourceType Complex { get; set; }
}
public class NestedDestinationType
{
public int ID { get; set; }
public string Name { get; set; }
}
public class ComplexDestinationType
{
public int ID { get; set; }
public NestedDestinationType Complex { get; set; }
}
Код руководства для этого:
var destination = new ComplexDestinationType
{
ID = source.ID,
Complex = new NestedDestinationType
{
ID = source.Complex.ID,
Name = source.Complex.Name
}
};
Проблема в том, что когда я компилирую LambdaExpression
и сравнить полученную delegate
это примерно в 10 раз медленнее, чем ручная версия. Я понятия не имею, почему это так. И вся идея в этом заключается в максимальной производительности без утомительного ручного картирования.
Когда я беру код от Барта де Смета из его поста в блоге на эту тему и сравниваю ручную версию вычисления простых чисел с скомпилированным деревом выражений, они совершенно идентичны по производительности.
Что может вызвать эту огромную разницу, когда отладочный вид LambdaExpression
похоже на то, что вы ожидаете?
РЕДАКТИРОВАТЬ
По запросу я добавил эталонный тест, который использовал:
public static ComplexDestinationType Foo;
static void Benchmark()
{
var mapper = new DefaultMemberMapper();
var map = mapper.CreateMap(typeof(ComplexSourceType),
typeof(ComplexDestinationType)).FinalizeMap();
var source = new ComplexSourceType
{
ID = 5,
Complex = new NestedSourceType
{
ID = 10,
Name = "test"
}
};
var sw = Stopwatch.StartNew();
for (int i = 0; i < 1000000; i++)
{
Foo = new ComplexDestinationType
{
ID = source.ID + i,
Complex = new NestedDestinationType
{
ID = source.Complex.ID + i,
Name = source.Complex.Name
}
};
}
sw.Stop();
Console.WriteLine(sw.Elapsed);
sw.Restart();
for (int i = 0; i < 1000000; i++)
{
Foo = mapper.Map<ComplexSourceType, ComplexDestinationType>(source);
}
sw.Stop();
Console.WriteLine(sw.Elapsed);
var func = (Func<ComplexSourceType, ComplexDestinationType, ComplexDestinationType>)
map.MappingFunction;
var destination = new ComplexDestinationType();
sw.Restart();
for (int i = 0; i < 1000000; i++)
{
Foo = func(source, new ComplexDestinationType());
}
sw.Stop();
Console.WriteLine(sw.Elapsed);
}
Второй, очевидно, медленнее, чем делать это вручную, так как включает в себя поиск по словарю и несколько экземпляров объекта, но третий должен быть таким же быстрым, как и вызываемый там необработанный делегат, и приведение из Delegate
в Func
происходит вне цикла.
Я также попытался обернуть ручной код в функцию, но я помню, что это не имело заметного значения. В любом случае, вызов функции не должен добавлять порядок издержек.
Я также дважды делаю тест, чтобы убедиться, что JIT не мешает.
РЕДАКТИРОВАТЬ
Вы можете получить код для этого проекта здесь:
https://github.com/JulianR/MemberMapper/
Я использовал расширение отладчика Sons-of-Strike, как описано в этом сообщении в блоге Барта де Смета, чтобы вывести сгенерированный IL динамического метода:
IL_0000: ldarg.2
IL_0001: ldarg.1
IL_0002: callvirt 6000003 ComplexSourceType.get_ID()
IL_0007: callvirt 6000004 ComplexDestinationType.set_ID(Int32)
IL_000c: ldarg.1
IL_000d: callvirt 6000005 ComplexSourceType.get_Complex()
IL_0012: brfalse IL_0043
IL_0017: ldarg.1
IL_0018: callvirt 6000006 ComplexSourceType.get_Complex()
IL_001d: stloc.0
IL_001e: newobj 6000007 NestedDestinationType..ctor()
IL_0023: stloc.1
IL_0024: ldloc.1
IL_0025: ldloc.0
IL_0026: callvirt 6000008 NestedSourceType.get_ID()
IL_002b: callvirt 6000009 NestedDestinationType.set_ID(Int32)
IL_0030: ldloc.1
IL_0031: ldloc.0
IL_0032: callvirt 600000a NestedSourceType.get_Name()
IL_0037: callvirt 600000b NestedDestinationType.set_Name(System.String)
IL_003c: ldarg.2
IL_003d: ldloc.1
IL_003e: callvirt 600000c ComplexDestinationType.set_Complex(NestedDestinationType)
IL_0043: ldarg.2
IL_0044: ret
Я не эксперт в IL, но это кажется довольно простым и именно то, что вы ожидаете, нет? Тогда почему это так медленно? Никаких странных операций с боксом, никаких скрытых реализаций, ничего. Это не совсем то же самое, что дерево выражений выше, так как есть также null
проверить right.Complex
сейчас.
Это код для ручной версии (полученной через Reflector):
L_0000: ldarg.1
L_0001: ldarg.0
L_0002: callvirt instance int32 ComplexSourceType::get_ID()
L_0007: callvirt instance void ComplexDestinationType::set_ID(int32)
L_000c: ldarg.0
L_000d: callvirt instance class NestedSourceType ComplexSourceType::get_Complex()
L_0012: brfalse.s L_0040
L_0014: ldarg.0
L_0015: callvirt instance class NestedSourceType ComplexSourceType::get_Complex()
L_001a: stloc.0
L_001b: newobj instance void NestedDestinationType::.ctor()
L_0020: stloc.1
L_0021: ldloc.1
L_0022: ldloc.0
L_0023: callvirt instance int32 NestedSourceType::get_ID()
L_0028: callvirt instance void NestedDestinationType::set_ID(int32)
L_002d: ldloc.1
L_002e: ldloc.0
L_002f: callvirt instance string NestedSourceType::get_Name()
L_0034: callvirt instance void NestedDestinationType::set_Name(string)
L_0039: ldarg.1
L_003a: ldloc.1
L_003b: callvirt instance void ComplexDestinationType::set_Complex(class NestedDestinationType)
L_0040: ldarg.1
L_0041: ret
Выглядит идентично мне..
РЕДАКТИРОВАТЬ
Я перешел по ссылке в ответе Майкла Б. на эту тему. Я попытался реализовать трюк в принятом ответе, и это сработало! Если вы хотите получить краткую информацию об уловке: он создает динамическую сборку и компилирует дерево выражений в статический метод в этой сборке, и по какой-то причине это в 10 раз быстрее. Недостатком этого является то, что мои эталонные классы были внутренними (на самом деле, публичные классы вложены во внутренний), и это вызвало исключение, когда я попытался получить к ним доступ, потому что они не были доступны. Кажется, что нет обходного пути, но я могу просто определить, являются ли ссылочные типы внутренними или нет, и решить, какой подход к компиляции использовать.
Однако меня все еще беспокоит то, что этот метод простых чисел по производительности идентичен скомпилированному дереву выражений.
И снова, я приветствую всех, кто запускает код в этом репозитории GitHub, чтобы подтвердить мои измерения и убедиться, что я не сумасшедший:)
5 ответов
Это довольно странно для такого огромного подслушивания. Есть несколько вещей, которые нужно принять во внимание. Во-первых, к скомпилированному коду VS применяются различные свойства, которые могут по-разному влиять на джиттер.
Включаете ли вы первое выполнение для скомпилированного делегата в эти результаты? Вы не должны, вы должны игнорировать первое выполнение любого пути кода. Вы также должны превратить обычный код в делегат, так как вызов делегата немного медленнее, чем вызов метода экземпляра, который медленнее, чем вызов статического метода.
Что касается других изменений, то есть что-то, что нужно учитывать, потому что скомпилированный делегат имеет закрывающий объект, который здесь не используется, но означает, что это целевой делегат, который может работать немного медленнее. Вы заметите, что скомпилированный делегат имеет целевой объект, и все аргументы сдвинуты на единицу.
Кроме того, методы, сгенерированные lcg, считаются статическими, которые, как правило, медленнее при компиляции делегатам, чем методы экземпляров, из-за бизнеса переключения регистров. (Даффи сказал, что указатель "this" имеет зарезервированный регистр в CLR, и когда у вас есть делегат для статического объекта, он должен быть перемещен в другой регистр, вызывая небольшие издержки). Наконец, код, сгенерированный во время выполнения, кажется, работает немного медленнее, чем код, сгенерированный VS. Код, сгенерированный во время выполнения, кажется, имеет дополнительную изолированную программную среду и запускается из другой сборки (попробуйте использовать что-то вроде кода операции ldftn или кода вызова calli, если вы мне не верите, эти делегаты mirror.emited будут компилироваться, но не позволят вам фактически выполнить их), который вызывает минимальные накладные расходы.
Также вы работаете в режиме релиза правильно? Была похожая тема, в которой мы рассматривали эту проблему: почему Func<> создается из Expression
Изменить: Также см. Мой ответ здесь: DynamicMethod намного медленнее, чем скомпилированная функция IL
Основной вывод заключается в том, что вы должны добавить следующий код в сборку, в которой вы планируете создавать и вызывать сгенерированный во время выполнения код.
[assembly: AllowPartiallyTrustedCallers]
[assembly: SecurityTransparent]
[assembly: SecurityRules(SecurityRuleSet.Level2,SkipVerificationInFullTrust=true)]
И всегда использовать встроенный тип делегата или один из сборки с этими флагами.
Причина в том, что анонимный динамический код размещается в сборке, которая всегда помечается как частичное доверие. Разрешая частично доверенных абонентов, вы можете пропустить часть рукопожатия. Прозрачность означает, что ваш код не будет повышать уровень безопасности (т. Е. Медленное поведение), и, наконец, реальная хитрость заключается в том, чтобы вызвать тип делегата, размещенный в сборке, которая помечена как пропускающая проверка. Func<int,int>#Invoke
полностью доверенный, поэтому проверка не требуется. Это даст вам производительность кода, сгенерированного из компилятора VS. Не используя эти атрибуты, вы смотрите на издержки в.NET 4. Вы можете подумать, что SecurityRuleSet.Level1 будет хорошим способом избежать этих издержек, но переключение моделей безопасности также дорого.
Короче говоря, добавьте эти атрибуты, и тогда ваш тест производительности микро-цикла будет примерно таким же.
Вы можете составить Expression Tree вручную через Reflection.Emit
, Как правило, он обеспечивает более быстрое время компиляции (в моем случае ниже ~ в 30 раз быстрее) и позволяет настраивать производительность на выходе. И это не так сложно сделать, особенно если ваши выражения ограничены известным подмножеством.
Идея состоит в том, чтобы использовать ExpressionVisitor
пройти через выражение и выдать IL для соответствующего типа выражения. Также "довольно" просто написать свой собственный посетитель для обработки известного подмножества выражений и возврата к нормальному состоянию. Expression.Compile
для еще не поддерживаемых типов выражений.
В моем случае я генерирую делегата:
Func<object[], object> createA = state =>
new A(
new B(),
(string)state[11],
new ID[2] { new D1(), new D2() }) {
Prop = new P(new B()), Bop = new B()
};
Тест создает соответствующее дерево выражений и сравнивает его Expression.Compile
против посещения и испускания IL, а затем создания делегата из DynamicMethod
,
Результаты, достижения:
Компиляция выражения 3000 раз: 814
Вызовите скомпилированное выражение 5000000 раз: 724
Испускать из выражения 3000 раз: 36
Выполнить испущенное выражение 5000000 раз: 722
36 против 814 при компиляции вручную.
Здесь полный код.
Похоже, вы сталкиваетесь с вызовом. Однако, независимо от источника, если ваш метод работает быстрее при загрузке из скомпилированной сборки, просто скомпилируйте его в сборку и загрузите! См. Мой ответ в разделе Почему Func<> создается из Expression
Проверьте эти ссылки, чтобы увидеть, что происходит, когда вы компилируете LambdaExpression
(и да, это делается с помощью Reflection)
Я думаю, что это влияние отражения в этой точке. Второй метод использует отражение, чтобы получить и установить значения. Насколько я понимаю, на данный момент это не делегат, а размышление, которое стоит своего времени.
О третьем решении: Лямбда-выражения также необходимо оценивать во время выполнения, что также стоит времени. И это не мало...
Таким образом, вы никогда не получите второе и третье решение так быстро, как ручное копирование.
Посмотрите на мои примеры кода здесь. Подумайте, что это быстрое решение, которое вы можете использовать, если не хотите ручного кодирования: http://jachman.wordpress.com/2006/08/22/2000-faster-using-dynamic-method-calls/