Производительность скомпилированного выражения для делегата - PullRequest
31 голосов
/ 19 февраля 2011

Я создаю дерево выражений, которое отображает свойства из исходного объекта в целевой объект, который затем компилируется в 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, чтобы подтвердить мои измерения и убедиться, что я не сумасшедший:)

Ответы [ 5 ]

19 голосов
/ 02 марта 2011

Это довольно странно для такого огромного подслушивания. Есть несколько вещей, которые необходимо принять во внимание. Во-первых, к скомпилированному коду VS применяются различные свойства, которые могут по-разному влиять на джиттер.

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

Что касается других изменений, то необходимо учитывать тот факт, что скомпилированный делегат имеет объект замыкания, который здесь не используется, но означает, что это целевой делегат, который может работать немного медленнее. Вы заметите, что у скомпилированного делегата есть целевой объект, и все аргументы сдвинуты на единицу.

Также методы, сгенерированные lcg, рассматриваются как статические, которые, как правило, работают медленнее при компиляции с делегатами, чем методы экземпляра из-за бизнеса переключения регистров. (Даффи сказал, что указатель «this» имеет зарезервированный регистр в CLR, и когда у вас есть делегат для статического объекта, он должен быть перемещен в другой регистр, вызывая небольшие издержки). Наконец, код, сгенерированный во время выполнения, кажется, работает немного медленнее, чем код, сгенерированный VS. Код, сгенерированный во время выполнения, кажется, имеет дополнительную изолированную программную среду и запускается из другой сборки (попробуйте использовать что-то вроде кода операции ldftn или кода вызова calli, если вы мне не верите, эти делегаты mirror.emited будут компилироваться, но не позволят вам фактически выполнить их ), который вызывает минимальные накладные расходы.

Также вы работаете в режиме релиза, верно? Была похожая тема, где мы рассмотрели эту проблему здесь: Почему Func <> создается из Expression > медленнее, чем Func <>, объявленный напрямую?

Edit: Также смотрите мой ответ здесь: DynamicMethod намного медленнее, чем скомпилированная функция IL

Основной вывод заключается в том, что вы должны добавить следующий код в сборку, в которой вы планируете создавать и вызывать сгенерированный во время выполнения код.

[assembly: AllowPartiallyTrustedCallers]
[assembly: SecurityTransparent]
[assembly: SecurityRules(SecurityRuleSet.Level2,SkipVerificationInFullTrust=true)]

И всегда использовать встроенный тип делегата или один из сборки с этими флагами.

Причина в том, что анонимный динамический код размещается в сборке, которая всегда помечается как частичное доверие. Разрешая частично доверенных абонентов, вы можете пропустить часть рукопожатия. Прозрачность означает, что ваш код не собирается повышать уровень безопасности (то есть медленное поведение), и, наконец, реальная хитрость заключается в том, чтобы вызвать тип делегата, размещенный в сборке, которая помечена как пропускающая проверка. Func<int,int>#Invoke полностью доверенный, поэтому проверка не требуется. Это даст вам производительность кода, сгенерированного из компилятора VS. Не используя эти атрибуты, вы смотрите на издержки в .NET 4. Вы можете подумать, что SecurityRuleSet.Level1 будет хорошим способом избежать этих издержек, но переключение моделей безопасности также дорого.

Короче говоря, добавьте эти атрибуты, и тогда ваш тест производительности микро-цикла будет примерно таким же.

3 голосов
/ 04 марта 2011

Звучит так, будто вы сталкиваетесь с вызовом. Однако, независимо от источника, если ваш метод работает быстрее при загрузке из скомпилированной сборки, просто скомпилируйте его в сборку и загрузите! См. Мой ответ на Почему Func <> создается из Expression > медленнее, чем Func <>, объявленный напрямую? для получения дополнительной информации о том, как.

2 голосов
/ 24 ноября 2015

Вы можете скомпилировать дерево выражений вручную через 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 при компиляции вручную.

Здесь полный код .

2 голосов
/ 04 марта 2011

Проверьте эти ссылки, чтобы увидеть, что происходит, когда вы компилируете LambdaExpression (и да, это делается с помощью Reflection)

  1. http://msdn.microsoft.com/en-us/magazine/cc163759.aspx#S3
  2. http://blogs.msdn.com/b/ericgu/archive/2004/03/19/92911.aspx
1 голос
/ 20 февраля 2011

Я думаю, что это влияние отражения в этой точке. Второй метод использует отражение, чтобы получить и установить значения. Насколько я понимаю, на данный момент это не делегат, а отражение, которое стоит своего времени.

О третьем решении. Также лямбда-выражения необходимо оценивать во время выполнения, что также стоит времени. И это не мало ...

Таким образом, вы никогда не получите второе и третье решение так быстро, как ручное копирование.

Посмотрите мои примеры кода здесь. Подумайте, что это быстрое решение, которое вы можете использовать, если не хотите ручного кодирования: http://jachman.wordpress.com/2006/08/22/2000-faster-using-dynamic-method-calls/

...