C#のMicro-ORM(Dapper, Massive, PetaPoco)について

最近、巷という名の極一部で話題になっているMicro-ORMという分野。何それ?うーん、MicroなORマッパーです。まんまですが。Entity FrameworkがUltra Hugeだとしたら、その対極にあるような。定義としては「SQL本文は手書き、マッピングは自動」だけの機能を持つもの、といったところでしょうか。Microであるために「1ファイルのみ」を入れても良さそう。少なくとも、用語の発生元っぽいDapper, Massive, PetaPocoは1ファイルのみです。

他に、シンプルめなORマッパーを志している感じなのはSimple.Dataや、そしてORマッパーじゃなくてただの実行機です!と言い張る私の作っているDbExecutorなどもMicro-ORMに入れちゃってもいいかしら?Simple.Dataは名前に反してあまりシンプルではない(MEF使ってる……)感じですが。DbExecutorは十分Microです!id:taediumさんの作られているSomaも比較的シンプルめなものに入るかしら……?

この中で注目株はDapperで、Stack Overflowのエンジニアがパフォーマンスの問題を解消するために作成したもの、ということで、超巨大サイトでの利用で鍛えられているというのは何よりも信頼への担保がある。そして、この手のO/Rマッパーにはかかせない動的コード生成によるパフォーマンス向上に関しては、パフォーマンスのエキスパート中のエキスパートであるprotobuf-netの作者 Marc Gravell氏がIL生成部分を担当しているという、凄いコンビでそりゃ色々と叶わないね、という感。

パフォーマンステスト

Dapperは親切にもベンチマークプログラムも公開しているので、リポジトリにあるもの+DbExecutor、それとDataTableを追加して計測してみました。

hand coded took 53ms
Mapper Query (buffered) took 55ms
Dynamic Mapper Query (non-buffered) took 55ms
Mapper Query (non-buffered) took 56ms
Dynamic Mapper Query (buffered) took 56ms
Dapper.Cotrib took 56ms
PetaPoco (Fast) took 58ms
DbExecutor Select took 58ms
DbExecutor ExecuteReader(Hand Coded) took 58ms Dynamic Massive ORM Query took 62ms
PetaPoco (Normal) took 62ms
DbExecutor SelectDynamic took 62ms
DbExecutor ExecuteReaderDynamic(Hand Coded) 65ms
DataTable took 83ms
BLToolkit took 85ms
Simple.Data took 90ms
Linq 2 SQL Compiled took 100ms
SubSonic Coding Horror took 114ms
Entity framework CompiledQuery took 119ms
NHibernate SQL took 127ms
NHibernate HQL took 149ms
Soma took 168ms
NHibernate Criteria took 191ms
Linq 2 SQL ExecuteQuery took 215ms
Linq 2 SQL took 671ms
NHibernate LINQ took 708ms
Entity framework ExecuteStoreQuery took 726ms
Entity framework ESQL took 728ms
Entity framework No Tracking took 966ms
Entity framework took 969ms
SubSonic ActiveRecord.SingleOrDefault took 4259ms

hand codedがExecuteReaderを手で回した手書き、「Mapper Query」はDapperのことです。複数種類があるのはオプション違い。DbExecutor(太字にしています)も同様に4種類で測っています。

結果ですが、勿論Dapperは速いんですが、DbExecutorのSelectも悪くない位置にある。というか、これは普通に高速と名乗っていいレベルの速度は出てる。というか上位陣はほとんど誤差でいいんじゃないですかというところですね、実際何回か測ると若干入れ替わったりしますし。500回のループで3ms遅くて低速とか言われたら怒ります(笑)

ところでDbExecutorのDynamic類が十分すぎるほど速いのは少し驚いたり(dynamic経由だからもっとずっと遅くなるのかと思ってた……)。この計測結果を前にすると、手動マッピングするならExecuteReaderはダルいからExecuteReaderDynamic使いますねー、型変換とか不要でずっとシンプルに書けますから。Selectが使えるシーンではSelectで、柔軟なマッピングをする必要があるシーンではExecuteReaderDynamicで、というのがDbExecutorを使う場合の幸せシナリオになりそう。また、ExpandoObjectをDataTableのRowの代わりとして使うSelectDynamicも十分な速度が出ていて、これぐらい速度出るなら普通に使っちゃえますねえ。非常に良い感じ。

何故速いのか、あるいは何故遅いのか

動的コード生成しているから速い。といっても、真ん中ぐらいより上のコードはみんなやっているのではないかしら。勿論、DbExecutorも生成しています。では何でDbExecutorは大体の場合において僅差とはいえDapperより遅いのか。これはコード生成する範囲の問題です。Dapperはクエリ用に最適化してガッツシ固めて生成・キャッシュしてますが、DbExecutorは汎用的に、Typeに対して行っているので、そこで若干の差が出ています。

最初Dapperのコード見たときは、うわあ、これは凄い差がついちゃってるのでは?とか思ったんですが、蓋を開けてみると、誤差みたいに小さな差でたいしたことなかったので、このまんまで行きます(それと言い訳がましいですがCode Contractとかのハンデも若干あるので、どの程度響いているかは不明瞭ですが)。どちらにせよ、ようするところ、DBへのアクセス速度に比べれば、その程度のチューニングは大して差が出ないということでしょう。

動的コード生成も、ILでゴリゴリじゃなくてExpressionTreeを使ったゆとり全開の生成なので、それで上位にガッツリ肉薄しているのだから、十分以上です。

まとめ

DbExecutorは普通に速い。他のと比べると気が効いていて、かつ洗練されたAPIを持っていて、非常に使いやすいので、Dapperなどと比べても負けてない。Micro-ORMの最前線で全然戦えます(但し1ファイルではないけれど!)。次のアップデートでは、クエリ時の戻りが複数になる場合の対応と、ストアドプロシージャのOUTPUTの対応を予定してますので是非是非お試しを。

と、宣伝がましくなってて申し訳ないですね……。Micro-ORM自体については、まず、Entity Frameworkなど普通のORMを使わないようなら、必需品だと思います。完全手作業でデータベース触るのは馬鹿げてる。何らかの薄いラッパーぐらいは作っているだろうけれど、Dapperよりも優れていると確信持てなければ(そして多くの場合は持てるわけがない!)、そんなものは捨ててDapperを使ったほうがいいのではないかしら。いや、DbExecutorでもいいですけどね、というかDbExecutorは良いですよ。

Entity Frameworkなどを使っている場合はどうか、というと、重量級フレームワークの欠けた部分を補ってやる感じで、良い感じに使えそうです。その辺の小回りの良さ、大きめなものと一緒に使っても上手く馴染むのはMicro-ORMならではなのではかと思います。

Profile

Yoshifumi Kawai

Cysharp, Inc
CEO/CTO

Microsoft MVP for Developer Technologies(C#)
April 2011
|
July 2024

Twitter:@neuecc GitHub:neuecc

Archive