ソースオブジェクトから宛先オブジェクトにプロパティをマップする式ツリーを生成しています。このツリーは、にコンパイルされて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つ目は、呼び出されている生のデリゲートと同じくらい高速であり、からのキャストはループの外側で発生しますDelegate
。Func
手動コードを関数でラップしようとしましたが、目立った違いはなかったことを思い出します。いずれにせよ、関数呼び出しは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リポジトリでコードを実行して、測定値を確認し、気が狂っていないことを確認してください:)