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