Quantcast
Channel: C#タグが付けられた新着記事 - Qiita
Viewing all articles
Browse latest Browse all 9364

C#で世界最速のMapperライブラリを作ってみた(AutoMapperなどよりも3倍-10倍ほど高速)

$
0
0

概要

CodeProjectへ投稿した英語の記事を日本語に訳してQiitaでも共有します。
https://www.codeproject.com/Articles/5275388/HigLabo-Mapper-Creating-Fastest-Object-Mapper-in-t

HigLaboMapperというオブジェクトマッパーを作ってみました。せっかく作るならということで世界最速を目標に作ってみました。
ExpressionTreeを使用して既存のマッパーライブラリであるAutoMapper,ExpressMapper,AgileMapper,FastMapper,Mapsterよりもはるかに高速な実装になっています。その結果、BenchmarkDotNetによるテスト結果で現在世界最速です。
また初期設定が不要で利用できるので無駄な設定コードを書く必要がありません。マッピングルールのカスタマイズも直感的な方法で自由にできるようになっています。

イントロダクション

4年前にオブジェクトマッパーをilコードにて作成しました。7月に思い立って実装しなおしてみようと思い、HigLabo.MapperをExpression Treeで書き直してみました。その結果、パフォーマンスを大幅に向上できました。パフォーマンステストの結果、2020年8月現在、世界最速です。作成はだいたい10日間くらいでできました。10日間のコミットログは
https://github.com/higty/higlabo.netstandard/tree/master/HigLabo.Mapper.
にあります。

せっかく作って埋もれさせておくのもアレなので共有してC#と.NETコミュニティに貢献しようかと思います。

ソースコードは
https://github.com/higty/higlabo/tree/master/NetStandard/HigLabo.Mapper.
にあります。

またNugetからHigLabo.Mapperで取得できます。

使い方

HigLabo.MapperをNugetからインストールします。バージョン3.0以降をインストールしてください。

名前空間をインポートします。

usingHigLabo.Core

これでMap拡張メソッドが利用可能になります。

vara1=newAddress();//your POCO class.vara2=a1.Map(newAddress());

HigLabo.MapperはDictionary to Objectのマッピングもサポートしています。

vard=newDictionary<String,String>();d["Name"]="Bill";varperson=d.Map(newPerson());//person.Name is "Bill"

Object to Dictionaryのマッピングも同様にサポートしています。

varp=newPerson();p.Name="Bill";vard=p.Map(newDictionary<String,String>);//d["Name"] is "Bill"

HigLabo.Mapperは直感的に使いやすいようにデザインされています。

他のマッパーとの比較

このセクションでは他のマッパーとの違いを解説していきます。目次は以下になります。
1. パフォーマンス
2. 初期設定
3. カスタマイズ
4. 複数の設定

パフォーマンス

マッパーライブラリではパフォーマンスが非常に重要です。マッピング処理はループの内部などホットコードパスで実行されることが多いのでパフォーマンスへの影響が大きくなりがちな傾向があります。

パフォーマンステスト結果のサマリーは以下になります。
・AutoMapperよりも3倍―4倍高速(コレクションプロパティの無いPOCOオブジェクト)
・Mapsterよりも10%-20%高速(コレクションプロパティの無いPOCOオブジェクト)
・AgileMapper, FastMapper, TinyMapperよりも7倍―10倍高速(コレクションプロパティの無いPOCOオブジェクト)
・AutoMapperよりも3倍高速(コレクションプロパティのあるPOCOオブジェクト)
・Mapsterよりも10倍高速(コレクションプロパティのあるPOCOオブジェクト)
・AgileMapper, FastMapper, TinyMapperよりも10倍―20倍高速(コレクションプロパティのあるPOCOオブジェクト)

テスト結果は以下のようになります。HigLaboObjectMapper_XXXが新しく作ったバージョン、HigLaboObjectMapConfig_XXXが古いバージョンになります。
HigLabo.Mapper.PerformanceTestResult_Mini1.png

テストに使用したPOCOクラスは以下のようなクラスです。

publicclassAddress{publicintId{get;set;}publicstringStreet{get;set;}publicstringCity{get;set;}publicstringCountry{get;set;}publicAddressTypeAddressType{get;set;}}publicclassAddressDTO{publicintId{get;set;}publicstringCity{get;set;}publicstringCountry{get;set;}publicAddressTypeAddressType{get;set;}=AddressType.House;}publicstructGpsPosition{publicdoubleLatitude{get;privateset;}publicdoubleLongitude{get;privateset;}publicGpsPosition(doublelatitude,doublelongitude){this.Latitude=latitude;this.Longitude=longitude;}}publicclassCustomer{publicInt32?Id{get;set;}publicStringName{get;set;}publicAddressAddress{get;set;}publicAddressHomeAddress{get;set;}publicAddress[]AddressList{get;set;}publicIEnumerable<Address>WorkAddressList{get;set;}}publicclassCustomerDTO{publicInt32?Id{get;set;}publicstringName{get;set;}publicAddressAddress{get;set;}publicAddressDTOHomeAddress{get;set;}publicAddressDTO[]AddressList{get;set;}publicList<AddressDTO>WorkAddressList{get;set;}publicStringAddressCity{get;set;}}

4つのパターンでテストをしてみました。

// 1. POCO class without collection property to same class.XXX.Map(newAddress(),newAddress());// 2. POCO class without collection property to other class.XXX.Map(newAddress(),newAddressDTO());// 3. POCO class that has collection property map to same class.XXX.Map(newCustomer(),newCustomer());// 4. POCO class that has collection property map to other class.XXX.Map(newCustomer(),newCustomerDTO());

添付した画像の結果のとおり全ての場合でHigLabo.Mapperが最速です。
テストコードはこちらになります。
https://github.com/higty/higlabo/tree/master/NetStandard/HigLabo.Test/HigLabo.Mapper.PerformanceTest

初期設定

いくつかのマッパーは使用前に初期設定が必要だったりします。

varconfiguration=newAutoMapper.MapperConfiguration(config=>{config.CreateMap<Building,Building>();config.CreateMap<TreeNode,TreeNode>();});

これはAutoMapperの設定コードです。もしマッピングするクラスが1000とかになってくるとこの作業は非常に退屈な作業で工数がかかります。

同様にTinyMapperも以下のような設定用のコードが必要です。

TinyMapper.Bind<Park,Park>();TinyMapper.Bind<Customer,CustomerDTO>();TinyMapper.Bind<Dictionary<String,String>,Building>();

HigLabo.Mapperでは上記の設定コードは不要です。設定コードを記述する工数をゼロにできます。

カスタマイズ

マッパーライブラリでは時々マッピングのルールをカスタマイズしたい時があります。AutoMapperのカスタマイズは非常に複雑で分かりずらい記述が必要です。
例えばこのページのマッピングルールで比較をしてみます。
https://stackoverflow.com/questions/50964757/delegating-member-mapping-to-child-object-with-automapper
AutoMapperでは

classSource{publicintId{get;set;}publicintUseThisInt{get;set;}publicInnerTypeInner{get;set;}// other properties that the Destination class is not interested in}classInnerType{publicintId{get;set;}publicintHeight{get;set;}// more inner properties}classDestination{publicintId{get;set;}publicintUseThisInt{get;set;}publicintHeight{get;set;}// more inner properties that should map to InnerType}//So many configuration and complicated....Mapper.Initialize(cfg=>{cfg.CreateMap<source,destination="">();cfg.CreateMap<innertype,destination="">();});vardest=Mapper.Map<destination>(src);Mapper.Map(src.Inner,dest);Mapper.Initialize(cfg=>{cfg.CreateMap<source,destination="">()AfterMap((src,dest)=>Mapper.Map(src.Inner,dest));cfg.CreateMap<innertype,destination="">();});vardest=Mapper.Map<destination>(src);

という感じで書く必要があり、AutoMapperのルール(Mapper.Initialize, ForMember, CreateMap, AfterMapなど)に精通する必要があります。

HigLabo.Mapperでのカスタマイズは非常に簡単です。

c.AddPostAction<Source,Destination>((s,d)=>{d.Id=s.Inner.Id;//Set Inner object property to Destination object     s.Inner.Map(d);});

HigLabo.Mapperではこのラムダ式がデフォルトのマップ処理が終わった後に呼ばれます。これにより既定のマッピングを上書きすることが可能です。

マッピング処理を完全に置き換えたい場合、ReplaceMapメソッドを利用します。

c.ReplaceMap<Source,Destination>((s,d)=>{//Set all map with your own.d.Id=s.Inner.Id;//Set Inner object property to Destination objects.Inner.Map(d);});//You can call Map method.varsource=newSource();vardestination=newDestination();source.Map(distination);//Above lambda will be called.

この方式はとてもシンプルで直感的に理解しやすいです。C#のラムダの知識があれば使用できます。C#をある程度使用しているならばラムダ式の知識は既にあるので追加の知識が必要とされることもありません。

コンバート処理も簡単に追加することが可能です。

c.AddPostAction<Person,PersonVM>((s,d)=>{d.BMI=CalculateBMI(s.Height,s.Weight);});

条件分岐によるマッピングも簡単です。

c.AddPostAction<Employee,EmployeeVM>((s,d)=>{if(s.EmployeeType==EmployeeType.Contract){d.Property1=someValue1;}else{d.Property1=someValue2;}});

もう一つ便利な点としてはデバッグが非常に容易という事が挙げられます。AddPostAction,ReplaceMapメソッドに渡したラムダ式の内部にブレイクポイントをセットしてデバッグが可能です。

プロパティのマッピングをカスタマイズすることも可能です。

classPerson{publicstringName{get;set;}publicstringPosition_Name{get;set;}}classPersonModel{publicstringName{get;set;}publicstringPositionName{get;set;}}varmapper=HigLabo.Core.ObjectMapper.Default;mapper.CompilerConfig.PropertyMatchRule=(sourceType,sourceProperty,targetType,targetProperty){if(sourceType==typeof(Person)&&targetType==typeof(PersonModel)){returnsourceProperty.Name.Replace("_","")==targetProperty.Name;}returnfalse;};

複数の設定

HigLabo.MapperではObjectMapperクラスのインスタンスを複数作成することが可能です。

varom1=newObjectMapper();om1.AddPostAction<Address,Address>((s,d)=>{//Custom map rule});varom2=newObjectMapper();om2.AddPostAction<Address,Address>((s,d)=>{//Another Custom map rule });vara=newAddress();vara1=om1.Map(a,newAddress());vara2=om1.Map(a,newAddress());

ObjectMapperExtensionsクラスで宣言されているMap拡張メソッドは実際にはObjectMapper.Defaultプロパティのインスタンスを使用しています。

usingSystem;namespaceHigLabo.Core{publicstaticclassObjectMapperExtensions{publicstaticTTargetMap<TSource,TTarget>(thisTSourcesource,TTargettarget){returnObjectMapper.Default.Map(source,target);}publicstaticTTargetMapOrNull<TSource,TTarget>(thisTSourcesource,Func<TTarget>targetConstructor)whereTTarget:class{returnObjectMapper.Default.MapOrNull(source,targetConstructor);}publicstaticTTargetMapOrNull<TSource,TTarget>(thisTSourcesource,TTargettarget)whereTTarget:class{returnObjectMapper.Default.MapOrNull(source,target);}publicstaticTTargetMapFrom<TTarget,TSource>(thisTTargettarget,TSourcesource){returnObjectMapper.Default.Map(source,target);}}}

複数のインスタンスを作成し、それぞれでマッピングのルールを設定して利用できます。アプリケーションのマッピングのルールをカプセル化することも可能です。

publicstaticclassObjectMapperExtensions{publicstaticvoidInitialize(thisObjectMappermapper){mapper.AddPostAction<Address,Address>((s,d)=>{//Your mapping rule.});mapper.AddPostAction<Address,Address>((s,d)=>{//Another your mapping rule.});}}//And call it on Application initialization process.ObjectMapper.Default.Initialize();

マッピングのテストケースについて

マッピングのテストケースは以下にあります。
https://github.com/higty/higlabo/tree/master/NetStandard/HigLabo.Test/HigLabo.Mapper.Test
1つのテストケースを除き、以前のバージョンの全てのテストケースをパスしてあります。
(※Dictionaryのカスタムマッピングは新しいバージョンでは未対応)

Deep Dive into エクスプレッションツリー

テストケースは以下にあります。
https://github.com/higty/higlabo/tree/master/NetStandard/HigLabo.Test/HigLabo.Mapper.Test
この中でObjectMapper_Map_ValueType_ValueTypeのテストケースで生成されるエクスプレッションツリーのコードは以下のような形になります。

.Lambda #Lambda1<System.Func`3[System.Object,System.Object,HigLabo.Mapper.Test.Vector2]>(
    System.Object $sourceParameter,
    System.Object $targetParameter) {
    .Block(
        HigLabo.Mapper.Test.Vector2 $source,
        HigLabo.Mapper.Test.Vector2 $target) {
        $source = .Unbox($sourceParameter);
        $target = .Unbox($targetParameter);
        .Call $target.set_X($source.X);
        .Call $target.set_Y($source.Y);
        $target
    }
}

AddressからAddressDTOへのマッピングでは以下のようなMapActionのFuncが生成されます。

.Lambda #Lambda1<System.Func`4[System.Object,System.Object,HigLabo.Core.ObjectMapper+MapContext,HigLabo.Mapper.PerformanceTest.AddressDTO]>(
    System.Object $sourceParameter,
    System.Object $targetParameter,
    HigLabo.Core.ObjectMapper+MapContext $context) {
    .Block(
        HigLabo.Mapper.PerformanceTest.Address $source,
        HigLabo.Mapper.PerformanceTest.AddressDTO $target) {
        $source = $sourceParameter .As HigLabo.Mapper.PerformanceTest.Address;
        $target = $targetParameter .As HigLabo.Mapper.PerformanceTest.AddressDTO;
        .Call $target.set_Id($source.Id);
        .Call $target.set_City($source.City);
        .Call $target.set_Country($source.Country);
        .Call $target.set_AddressType($source.AddressType);
        $target
    }
}

ほぼ無駄のないコードが生成されているのがわかると思います。コードを見てもわかる通りHigLabo.Mapperよりも最速なコードは生成しづらいでしょう。Spanなどを利用すればもっと早くなる可能性はあるかもしれません。

これらのエクスプレッションツリーのコードブロックはコンパイルされてFuncに変換され、プライベートな_MapActionListフィールドに保存されます。2回目以降はコンパイル済みのFuncが使用されるのでコンパイルのオーバーヘッドはありません。AddPostActionで渡したラムダはこのFunc後に呼び出されるように新しいFuncが生成されます。ReplaceMapメソッドを使用するとこのFuncを置き換えることになります。

まとめ

C#でマッピング処理を高速化したい、設定用のコードを削減したい、もっと簡単にカスタマイズしたいという人にこのライブラリが役に立つと嬉しいです。不具合などあれば気軽にGitHubにコメントください。

おまけ

ハイパフォーマンスなアプリケーション作成に興味がある人は以下の記事も参考になると思います。
タスク管理を作ってみた ~初級からレベルアップするためのツールとサービスたち~
パフォーマンス向上で知っておくべきコンピューターサイエンスの基礎知識とその実践
WEBアプリケーションのパフォーマンスをUPするために知っておくべき技術と知識
C#で世界最速のMapperライブラリを作ってみた(AutoMapperなどよりも3倍-10倍ほど高速)
プログラマのための機能のUIデザイン
世界で通用するエンジニアになるための高度な技術記事(英語)
Azure アプリケーション アーキテクチャ ガイド

元記事です↓
パフォーマンス向上で知っておくべきコンピューターサイエンスの基礎知識とその実践

またこういった知識を使ってSaasアプリを作る方法をこちらの記事で紹介しています。


Viewing all articles
Browse latest Browse all 9364

Latest Images

Trending Articles