Я генерирую дерево выражений, которое отображает свойства исходного объекта в целевой объект, который затем скомпилируется в 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, как описано в блоге Bart de Smet, чтобы сгенерировать сгенерированный 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
Выглядит идентично мне.
ИЗМЕНИТЬ
Я пошел по ссылке в Michael B, чтобы ответить на эту тему. Я попытался реализовать трюк в принятом ответе, и это сработало! Если вам нужна сводка трюка: она создает динамическую сборку и компилирует дерево выражений в статический метод в этой сборке и по какой-то причине в 10 раз быстрее. Недостатком этого является то, что мои эталонные классы были внутренними (фактически, публичные классы вложены во внутреннюю), и это вызвало исключение, когда я пытался получить к ним доступ, потому что они были недоступны. Кажется, что это не обходное решение, но я могу просто определить, являются ли указанные типы внутренними или нет, и решить, какой подход к компиляции использовать.
То, что все еще меня беспокоит, является причиной того, что метод простых чисел идентичен по производительности для скомпилированного дерева выражений.
И снова я приветствую всех, кто запускает код в этом репозитории GitHub, чтобы подтвердить мои измерения и убедиться, что я не сумасшедший:)