31

ソースオブジェクトから宛先オブジェクトにプロパティをマップする式ツリーを生成しています。このツリーは、にコンパイルされて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倍遅くなることです。それがなぜなのか私にはわかりません。そして、これに関する全体的な考え方は、手作業によるマッピングの面倒な作業なしで最大のパフォーマンスを実現することです。

このトピックに関する彼のブログ投稿からBartdeSmetによるコードを取得し、コンパイルされた式ツリーに対して素数を計算する手動バージョンのベンチマークを行うと、パフォーマンスは完全に同じになります。

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);
}

2つ目は、辞書のルックアップといくつかのオブジェクトのインスタンス化が含まれるため、手動で行うよりも当然低速ですが、3つ目は、呼び出されている生のデリゲートと同じくらい高速であり、からのキャストはループの外側で発生しますDelegateFunc

手動コードを関数でラップしようとしましたが、目立った違いはなかったことを思い出します。いずれにせよ、関数呼び出しは1桁のオーバーヘッドを追加するべきではありません。

また、JITが干渉していないことを確認するために、ベンチマークを2回実行します。

編集

このプロジェクトのコードは次の場所から入手できます。

https://github.com/JulianR/MemberMapper/

Bart de Smetによるブログ投稿で説明されている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 

私と同じように見えます。

編集

このトピックに関するMichaelBの回答のリンクをたどりました。私は受け入れられた答えにトリックを実装しようとしました、そしてそれはうまくいきました!トリックの要約が必要な場合:動的アセンブリを作成し、式ツリーをそのアセンブリ内の静的メソッドにコンパイルします。何らかの理由で、10倍高速です。これの欠点は、ベンチマーククラスが内部クラス(実際には、内部クラスにネストされたパブリッククラス)であり、アクセスできなかったためにアクセスしようとしたときに例外がスローされたことです。回避策はないようですが、参照されているタイプが内部であるかどうかを簡単に検出し、使用するコンパイルのアプローチを決定できます。

それでも私を悩ませているのは、その素数法のパフォーマンスがコンパイルされた式ツリーと同じである理由です。

繰り返しになりますが、GitHubリポジトリでコードを実行して、測定値を確認し、気が狂っていないことを確認してください:)

4

5 に答える 5

20

これは、そのような巨大な耳にしたことにはかなり奇妙です。考慮すべきことがいくつかあります。まず、VSでコンパイルされたコードにはさまざまなプロパティが適用されており、ジッターに影響を与えて最適化を変える可能性があります。

これらの結果には、コンパイルされたデリゲートの最初の実行が含まれていますか?すべきではありません。どちらかのコードパスの最初の実行を無視する必要があります。また、デリゲートの呼び出しはインスタンスメソッドの呼び出しよりもわずかに遅く、静的メソッドの呼び出しよりも遅いため、通常のコードをデリゲートに変換する必要があります。

その他の変更については、コンパイルされたデリゲートに、ここでは使用されていないクロージャーオブジェクトがあるという事実を説明するものがありますが、これは、パフォーマンスが少し遅くなる可能性のあるターゲットデリゲートであることを意味します。コンパイルされたデリゲートにターゲットオブジェクトがあり、すべての引数が1つ下にシフトされていることがわかります。

また、lcgによって生成されたメソッドは静的と見なされ、レジスタスイッチングビジネスのために、インスタンスメソッドよりもデリゲートにコンパイルされると遅くなる傾向があります。(Duffyは、「this」ポインターにはCLRに予約済みのレジスタがあり、スタティックのデリゲートがある場合は、わずかなオーバーヘッドを呼び出す別のレジスタにシフトする必要があると述べました)。最後に、実行時に生成されたコードは、VSによって生成されたコードよりもわずかに遅く実行されるようです。実行時に生成されたコードには余分なサンドボックスがあるようで、別のアセンブリから起動されます(信じられない場合は、ldftnオペコードやcalliオペコードなどを使用してみてください。これらのreflection.emitedデリゲートはコンパイルされますが、実際には実行できません。 )これは最小限のオーバーヘッドを呼び出します。

また、リリースモードで実行していますよね?ここでこの問題を調べた同様のトピックがありました 。Expression<Func<>>から作成されたFunc<>が、直接宣言されたFunc <>よりも遅いのはなぜですか?

編集:ここで私の答えも参照してください: DynamicMethodはコンパイルされたIL関数よりもはるかに遅いです

主なポイントは、実行時に生成されたコードを作成して呼び出す予定のアセンブリに次のコードを追加する必要があるということです。

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

また、組み込みのデリゲートタイプ、またはこれらのフラグを持つアセンブリからのデリゲートタイプを常に使用します。

その理由は、匿名の動的コードが、常に部分的な信頼としてマークされているアセンブリでホストされているためです。部分的に信頼された発信者を許可することにより、ハンドシェイクの一部をスキップできます。透過性とは、コードがセキュリティレベルを上げないことを意味します(つまり、動作が遅くなります)。最後に、実際のトリックは、スキップ検証としてマークされたアセンブリでホストされているデリゲートタイプを呼び出すことです。Func<int,int>#Invokeは完全に信頼されているため、検証は必要ありません。これにより、VSコンパイラから生成されたコードのパフォーマンスが得られます。これらの属性を使用しないことで、.NET 4のオーバーヘッドを確認できます。SecurityRuleSet.Level1はこのオーバーヘッドを回避するための良い方法だと思うかもしれませんが、セキュリティモデルの切り替えにもコストがかかります。

つまり、これらの属性を追加すると、マイクロループパフォーマンステストがほぼ同じように実行されます。

于 2011-03-01T21:23:15.910 に答える
3

呼び出しのオーバーヘッドが発生しているようです。ただし、ソースに関係なく、コンパイルされたアセンブリからロードしたときにメソッドの実行速度が速い場合は、単にアセンブリにコンパイルしてロードしてください。Expression <Func<>>から作成されたFunc<>がFunc<>を直接宣言するよりも遅いのはなぜですか?の私の答えを参照してください。方法の詳細については。

于 2011-03-04T13:54:13.653 に答える
2

これらのリンクをチェックして、コンパイル時に何が起こるかを確認してください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
于 2011-03-04T12:43:17.580 に答える
2

を介して式ツリーを手動でコンパイルできます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。

ここに完全なコードがあります。

于 2015-11-24T11:41:04.773 に答える
1

これが、この時点でリフレクションを使用した場合の影響だと思います。2番目の方法は、リフレクションを使用して値を取得および設定することです。この時点で私が見る限り、それは代表者ではなく、時間を費やす反省です。

3番目のソリューションについて:Lambda式も実行時に評価する必要があり、これにも時間がかかります。そして、それは少なくありません...

したがって、手動コピーほど速く2番目と3番目のソリューションを取得することはできません。

ここで私のコードサンプルを見てください。手動コーディングが必要ない場合は、これが適切に実行できる高速ソリューションであると考えてください。http: //jachman.wordpress.com/2006/08/22/2000-faster-using-dynamic-method-calls/

于 2011-02-19T21:32:35.643 に答える