Выполнение скомпилированного делегирования

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

Ответ 1

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

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

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

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

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

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

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

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

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

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

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

Ответ 2

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

Ответ 4

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

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

Ответ 5

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

О третьем решении: также выражения Lambda Expressions необходимо оценивать во время выполнения, что также требует времени. И это мало...

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

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