今日の記事では、.NET Standard 2.0用の独自のマッパーを作成する短い冒険についてお話ししたいと思います。 githubへのリンクとベンチマーク結果が添付されています。
マッパーとは何か、何のためにあるのかは誰にも秘密ではないと思います。 文字通り、作業プロセスのすべてのステップで、あるタイプから別のタイプへのデータのマッピング(または変換)のさまざまな例に直面しています。 これらには、レポジトリからドメインモデルへのレコードのマッピング、ビューモデルでのリモートサービスの応答応答マッピング、およびドメインモデルでのみが含まれます。 多くの場合、抽象化レベルの境界には入力と出力のデータ形式があり、マッパーのようなものがすべての栄光で自分自身を示すことができる抽象化の相互作用の瞬間に、開発者にとって時間と労力の大幅な節約をもたらし、結果として、システム全体のパフォーマンスのごく一部です。
これに基づいて、MVP要件を説明できます。
- 作業の速度(パフォーマンスとメモリへの影響が少ない);
- 使いやすさ(クリーンで使いやすいAPI)。
最初の点に関しては、最適化なしではなく、BenchmarkDotNetと思慮深い実装がこれに役立ちます。 第二に、私は単純な単体テストを書きました。それは何らかの方法で、マッパーのAPIのドキュメントとして機能します。
[TestMethod] public void WhenMappingExist_Then_Map() { var dto = new CustomerDto { Id = 42, Title = "Test", CreatedAtUtc = new DateTime(2017, 9, 3), IsDeleted = true }; mapper.Register<CustomerDto, Customer>(); var customer = mapper.Map<CustomerDto, Customer>(dto); Assert.AreEqual(42, customer.Id); Assert.AreEqual("Test", customer.Title); Assert.AreEqual(new DateTime(2017, 9, 3), customer.CreatedAtUtc); Assert.AreEqual(true, customer.IsDeleted); }
合計で、2つの単純なメソッドのみを実装する必要があります。
-
void Register<TSource, TDest>()
; -
TDest Map<TSource, TDest>(TSource source)
。
登録
実際、最初にMap
メソッドが呼び出されたときに登録プロセスを実行できます。これにより、不要になります。 ただし、次の理由で個別に取り出しました。
- 検証のために、私の意見ではデフォルトのコンストラクターがない(または最終型をマップできない)場合、構成段階でできるだけ早くこれを報告する必要があります。 そうしないと、型のインスタンスを作成できないというエラーが、インフラストラクチャコードまたはビジネスロジックの実行の段階ですでに追い越されている可能性があります。
- 拡張の場合、現時点ではAPIは非常にシンプルであり、内部では命名規則に基づいたマッピングを意味しますが、特定のフィールドのマッピングのルールを導入することはすぐに可能になる可能性が高く、その割り当ての値はメソッドの結果である可能性があります。 この場合、単一の責任原則に準拠するために、このような区分は非常に論理的なように思えます。
マッパーのMap
メソッドがメインであり、実行時間の大部分を占める場合、反対に、 Register
メソッドは、構成段階で各タイプペアに対して1回だけ呼び出されます。 そのため、彼はすべての必要な「重い」操作の最適な候補です。最適なマッピング実行計画を生成し、その結果、結果をさらにキャッシュします。
したがって、その実装には以下が含まれます。
- 必要なタイプのインスタンスを作成および初期化するための計画の構築。
- キャッシュ結果。
実行計画
C#では、ランタイムで型のインスタンスを作成および初期化する方法は多くありません。メソッドの抽象化レベルが高いほど、ランタイムの観点からは最適性が低くなります。 以前、私はFsContainerと呼ばれる他の小さなプロジェクトで既に同様の選択に直面していたため、次の結果は驚くことではありませんでした。
BenchmarkDotNet=v0.10.9, OS=Windows 8.1 (6.3.9600) Processor=Intel Core i5-5200U CPU 2.20GHz (Broadwell), ProcessorCount=4 Frequency=2143473 Hz, Resolution=466.5326 ns, Timer=TSC .NET Core SDK=2.0.0 [Host] : .NET Core 2.0.0 (Framework 4.6.00001.0), 64bit RyuJIT DefaultJob : .NET Core 2.0.0 (Framework 4.6.00001.0), 64bit RyuJIT
| Method | Mean | Error | StdDev | Median | |---------------------------- |-----------:|----------:|----------:|-----------:| | ExpressionCtorObjectBuilder | 8.548 ns | 0.2764 ns | 0.4541 ns | 8.608 ns | | ActivatorCreateInstance | 79.379 ns | 1.6812 ns | 3.1987 ns | 78.890 ns | | ConstructorInfoInvoke | 164.445 ns | 3.3355 ns | 4.3371 ns | 164.016 ns | | DynamicMethodILGenerator | 5.859 ns | 0.2455 ns | 0.3015 ns | 5.819 ns | | NewCtor | 6.989 ns | 0.2615 ns | 0.5741 ns | 6.756 ns |
ConstructorInfo.Invoke
とActivator.CreateInstance
使用Activator.CreateInstance
非常に簡単ですが、実装の詳細でRuntimeType
とSystem.Reflection
を使用しているため、明らかにこのリストでは部外者です。 これは日常のタスクではかなり許容されますが、パフォーマンスの観点から最も狭いボトルネックである型のインスタンスを作成するという要件では完全に不適切です。
Expression
とDynamicMethod
使用に関しては、ここで驚くことなく、実行の結果はコンパイルされた関数へのポインターであり、対応する引数を渡すことによってのみ呼び出すことができます。
実行中にILコードを生成してコンパイルされたデリゲートは、多少高速ですが、型インスタンスの初期化コードは含まれていません。 さらに、個人的には、 ilgen.Emit
を介してIL命令をilgen.Emit
は非常に重要なilgen.Emit
です。
var dynamicMethod = new DynamicMethod("Create_" + ctorInfo.Name, ctorInfo.DeclaringType, new[] { typeof(object[]) }); var ilgen = dynamicMethod.GetILGenerator(); ilgen.Emit(OpCodes.Newobj, ctorInfo); ilgen.Emit(OpCodes.Ret); return dynamicMethod.CreateDelegate(typeof(Func<TDest>));
それが、 Expression
を使用した実装に落ち着いた理由です。
var body = Expression.MemberInit( Expression.New(typeof(TDest)), props ); return Expression.Lambda<Func<TSource, TDest>>(body, orig).Compile();
キャッシング
後でマッピングの実行に使用されるコンパイル済みデリゲートをキャッシュするために、 Dictionary
とHashtable
どちらかを選択しました。 今後、キーの役割はコレクションのタイプだけでなく、選択が行われるキーのタイプによっても行われることに注意したいと思います。 この声明を検証するために、別のベンチマークが作成され、次の結果が得られました。
BenchmarkDotNet=v0.10.9, OS=Windows 8.1 (6.3.9600) Processor=Intel Core i5-5200U CPU 2.20GHz (Broadwell), ProcessorCount=4 Frequency=2143473 Hz, Resolution=466.5326 ns, Timer=TSC .NET Core SDK=2.0.0 [Host] : .NET Core 2.0.0 (Framework 4.6.00001.0), 64bit RyuJIT DefaultJob : .NET Core 2.0.0 (Framework 4.6.00001.0), 64bit RyuJIT
| Method | Mean | Error | StdDev | |-------------------- |----------:|----------:|----------:| | DictionaryTuple | 80.37 ns | 1.6473 ns | 1.6179 ns | | DictionaryTypeTuple | 49.35 ns | 0.6235 ns | 0.5832 ns | | HashtableTuple | 103.07 ns | 2.6081 ns | 2.4397 ns | | HashtableTypeTuple | 71.51 ns | 0.8679 ns | 0.7694 ns |
これを考慮して、次の結論を導き出すことができます。
- コレクションアイテムを取得するのにかかる時間の観点から、
Hashtable
よりもDictionary
タイプを使用することをおHashtable
します。 -
TypeTuple
( src )をキーとして使用することは、Equals
&GetHashCode
費やす時間の観点から、TypeTuple
Tuple<Type, Type>
よりも望ましいです。
マッピング
Map
メソッドの内部実装は、このメソッドが99.9%のケースで呼び出されるため、非常にシンプルで最適化される必要があります。 したがって、必要なのは、キャッシュ内で以前にコンパイルされたDelegate
へのリンクを見つけて、その実行結果を返すために、できるだけ早くすることだけです。
public TDest Map<TSource, TDest>(TSource source) { var key = new TypeTuple(typeof(TSource), typeof(TDest)); var activator = GetMap(key); return ((Func<TSource, TDest>)activator)(source); }
結果
結果として、現在の(および最新の)マッパーの最終的な測定結果を現時点で提供したいと思います。
BenchmarkDotNet=v0.10.9, OS=Windows 8.1 (6.3.9600) Processor=Intel Core i5-5200U CPU 2.20GHz (Broadwell), ProcessorCount=4 Frequency=2143473 Hz, Resolution=466.5326 ns, Timer=TSC .NET Core SDK=2.0.0 [Host] : .NET Core 2.0.0 (Framework 4.6.00001.0), 64bit RyuJIT DefaultJob : .NET Core 2.0.0 (Framework 4.6.00001.0), 64bit RyuJIT
| Method | Mean | Error | StdDev | |----------------------- |-----------:|----------:|----------:| | FsMapperBenchmark | 84.492 ns | 1.6972 ns | 1.6669 ns | | ExpressMapperBenchmark | 251.161 ns | 4.6736 ns | 4.3717 ns | | AutoMapperBenchmark | 204.142 ns | 4.2002 ns | 9.1309 ns | | MapsterBenchmark | 90.949 ns | 1.6393 ns | 1.4532 ns | | AgileMapperBenchmark | 218.021 ns | 3.0921 ns | 2.7410 ns | | CtorMapperBenchmark | 7.806 ns | 0.2472 ns | 0.2312 ns |
プロジェクトのソースコードはgithubで入手できます。 https://github.com/FSou1/FsMapper
最後まで読んでいただきありがとうございます。この記事がお役に立てば幸いです。 あなたの意見ではまだ最適化できるものをコメントで書いてください。