ORMベンチマーク(RawDataAccessBencher)をAzure IaaSで実行してみた

はじめに(雑談的な・・・)

新規のシステム開発を行う際に、最近 何気に悩むのが「データアクセスのアーキテクチャ選択」だと思います。
昔々20年前くらいであれば「SQLをADO経由でゴリゴリ実行するぞ!」でOKだったのが、今では「素のADO.NET」から「Entity Frameworkのような重量級(?)のORM(Object Relational Mapper)」「軽さを追い求めて作られたMicro ORMであるDapper」などなど・・・多岐に渡ります。

.NET開発におけるORMとしてはマイクロソフト公式技術として Entity Framework がありますが、ver.1.0 リリース時からスピードの遅さが指摘されていて、なんだか「これを採用だ!」と言いにくい雰囲気が作られているようにも思います。
で、このEntity Frameworkも気が付けば フルセット.NET用は Ver.6、.NET Core用は Ver1.0 RTM となっています。

SQL隠ぺい型ORM」か「SQL明示定義型ORM」か

ORMの種類を大きく分けると、「Entity FrameworkのようなSQLを意識しない方針のORM」と「DapperやMyBatisのようなSQLを強く意識する方針のORM」があります。
私は データベースエンジニア ではなく ソフトウェアエンジニア の人間なので、前者の世界の方が個人的には好みであり、理想の世界だと思っています。
ソフトウェアを開発するにあたり、アプリケーションロジックはC#Rubyのようなオブジェクト指向言語で記述し、「データベースアクセスにはSQLという、リレーショナルデータを操作するための手続き型言語を利用する」というのはソフトウェアアーキテクチャとしては煩雑なのです。SQLが書きたいわけではなく、欲しいデータを欲しいフォーマットで取得したいだけなのだから、そしてその型はドメイン分析の結果得られたクラスオブジェクト型であってほしいのです。
SQL世界を実現するのが、Entity Frameworkであり、LINQという言語ですよね。

とはいえ、高いパフォーマンス、高負荷状態でも高速なデータ取得をリレーショナルデータベースに求めると、やはりチューニングされたSQLを実行する事が最も高速に動作するというのも事実。そして、結構こういうケースが多いと思います。
なので、「データアクセスのアーキテクチャ選択」が難しいのだと思います。
選択肢としては1つの技術を選択するだけでなく、生産性重視のORM+速度重視の別技術、など、1システム内でも機能毎に利用する技術を別々にするのも選択肢だと思います。

ベンチマークしてみた

で、改めて各種データアクセス技術のベンチマークを取ってみよう、と思いました。
利用したのは「RawDataAccessBencher」。GitHub上で公開されている 素のADO.NET および 各種ORM でのクエリーパフォーマンスを測定するベンチマークプログラムです。
Githubページ上でも、ベンチマーク結果が公開されているので、自分で動かさなくてもベンチマーク結果を参照することはできますが、ここでは改めてAzure IaaS上でベンチマークを取ってみました。

Azure IaaSは「Standard A6 (4 コア、28 GB メモリ) / Windows Server 2016 / SQL Server 2016 RTM」の環境を利用しました。
Azure IaaSを新規に作っているので、まっさらな ベンチマークには最適な環境だと思います。

データベースの準備

RawDataAccessBencherは、SQL Serverサンプルデータベースである「AdventureWorks」を利用してベンチマーク測定を行います。
AdventureWorksデータベースは、以前はSQL Serverインストーラに標準で付属していましたが、2012あたり(?)から付属しなくなりました。
以下のURLからダウンロードする事が出来ます。

Microsoft SQL Server Product Samples: Database - Downloads

f:id:daigo-knowlbo:20161113163933p:plain

SQL Management Studioを起動し、Databaseノードでマウス右ボタンメニュー「Restore Database...」を選択します。

f:id:daigo-knowlbo:20161113184905p:plain

SourceからDeviceを選択し、ダウンロードした「AdventureWorks2014.bak」ファイルを選択して、OKボタンをクリックします。

f:id:daigo-knowlbo:20161113185013p:plain

以上でSQL Server 2016上に AdventureWorks2014 サンプルデータベースが復元されました。

RawDataAccessBencherのビルド

Github(https://github.com/FransBouma/RawDataAccessBencher)からソリューションをCloneするかzip downloadします。
Visual Studio 2015 で RawBencher.sln をオープンします。
構成を Release に設定します。

!! 注意点 !!

2つほど注意点があります。
【1点目】
RawDataAccessBencherは、データベースへの接続文字列をApp.config(RawBencher.exe.config)に保持しますが、デフォルトでは利用データベース名は「AdventureWorks」となっています。
しかし、先程リストアしたデータベースは AdventureWorks2014 というデータベース名となっています。その為、接続データベース名の修正が必要になります。
App.config(RawBencher.exe.config)の以下の箇所のコメントを解除して有効にします。

<section name="sqlServerCatalogNameOverwrites" type="System.Configuration.NameValueSectionHandler" />

続けて、以下の箇所のコメントを解除して、更にVlue値を以下のように AdventureWorks2014 とします。

<sqlServerCatalogNameOverwrites>
  <add key="AdventureWorks" value="AdventureWorks2014" />
</sqlServerCatalogNameOverwrites>

更に以下の箇所の data source 値を適時マシン名に、また、initial catalog 値を AdventureWorks2014 に変更します。

<add name="AdventureWorks.ConnectionString.SQL Server (SqlClient)" connectionString="data source=DESKTOP-LFQS0ND;initial catalog=AdventureWorks2014;integrated security=SSPI;persist security info=False;packet size=4096" providerName="System.Data.SqlClient" />
<add name="EF.ConnectionString.SQL Server (SqlClient)" connectionString="metadata=res://*/AW.csdl|res://*/AW.ssdl|res://*/AW.msl;provider=System.Data.SqlClient;provider connection string=&quot;data source=DESKTOP-LFQS0ND;initial catalog=AdventureWorks2014;integrated security=SSPI;persist security info=False;packet size=4096&quot;" providerName="System.Data.EntityClient" />

【2点目】
「RawBencher\OriginalController.cs」ソースコード内に1箇所 AdventureWorks がハードコーディングされています。
ソースの以下の箇所を修正します。

KeysForIndividualFetches = conn.Query<int>("select top {=count} SalesOrderId from AdventureWorks.Sales.SalesOrderHeader order by SalesOrderNumber", new { count = IndividualKeysAmount }).AsList();
↓↓↓ 修正 ↓↓↓
KeysForIndividualFetches = conn.Query<int>("select top {=count} SalesOrderId from AdventureWorks2014.Sales.SalesOrderHeader order by SalesOrderNumber", new { count = IndividualKeysAmount }).AsList();

以上でビルド準備完了です。
Release構成でビルドを行い、作成された「bin\Releaseフォルダ」をAzure IaaS環境にコピーします。

ベンチマーク実行

一応、SQL Serverをクリアな状態にする為に、以下のコマンドをSQL Serverに対して実行します。

DBCC DROPCLEANBUFFERS;
DBCC FREEPROCCACHE;

コマンドプロンプトを開き、先程ビルド / IaaSにコピーした RawBencher.exe を実行します。

RawBencher.exe /a > Result.txt

「/a」パラメータを付与します。また「>」により標準出力結果を result.txt ファイルに出力するようにします。 (/a オプションを付与しないと、処理終了後にユーザー入力待ちが行われてしまいます。処理実行後、結果をファイル出力しそのまま自動的にプログラム実行は完了したい為に /a オプションを利用しています。)

結果

結果は以下の通りです。

ベンチマーク結果をダウンロード

結果からログ出力以外の主要な結果を抽出したものは以下の通り

Results per framework. Values are given as: 'mean (standard deviation)'
==============================================================================
Non-change tracking fetches, set fetches (25 runs), no caching
------------------------------------------------------------------------------
Handcoded materializer using DbDataReader                            : 319.68ms (9.93ms)    Enum: 3.93ms (0.39ms)
Handcoded materializer using DbDataReader (GetValues(array), boxing) : 351.40ms (11.97ms)   Enum: 3.82ms (0.43ms)
LINQ to DB v1.0.7.3 (v1.0.7.3) (normal)                              : 370.69ms (11.33ms)   Enum: 2.90ms (0.39ms)
Raw DbDataReader materializer using object arrays                    : 372.05ms (24.42ms)   Enum: 6.66ms (0.44ms)
LINQ to DB v1.0.7.3 (v1.0.7.3) (compiled)                            : 378.16ms (15.66ms)   Enum: 2.92ms (0.40ms)
PetaPoco Fast v4.0.3                                                 : 381.83ms (7.74ms)    Enum: 3.97ms (0.47ms)
LLBLGen Pro v5.0.0.0 (v5.0.4), Poco typed view with QuerySpec        : 430.82ms (10.68ms)   Enum: 3.12ms (0.40ms)
PetaPoco v4.0.3                                                      : 438.12ms (12.51ms)   Enum: 4.04ms (0.42ms)
LLBLGen Pro v5.0.0.0 (v5.0.4), Poco typed view with Linq             : 448.98ms (25.86ms)   Enum: 2.93ms (0.48ms)
Dapper v1.50.0.0                                                     : 470.63ms (24.97ms)   Enum: 4.10ms (1.20ms)
ServiceStack OrmLite v4.0.60.0 (v4.0.60.0)                           : 473.27ms (18.74ms)   Enum: 3.75ms (0.42ms)
Entity Framework v1.0.0.0 (v1.0.0.20622)                             : 495.84ms (17.27ms)   Enum: 3.48ms (1.12ms)
Linq to Sql v4.0.0.0 (v4.6.1586.0)                                   : 508.53ms (14.41ms)   Enum: 3.37ms (0.45ms)
Entity Framework v6.0.0.0 (v6.1.40302.0)                             : 539.37ms (19.22ms)   Enum: 3.17ms (0.36ms)
LLBLGen Pro v5.0.0.0 (v5.0.4), DataTable based TypedView             : 903.03ms (19.57ms)   Enum: 12.62ms (2.57ms)
Massive using dynamic class                                          : 1,294.43ms (21.67ms) Enum: 54.08ms (7.80ms)
Oak.DynamicDb using dynamic Dto class                                : 1,619.00ms (86.53ms) Enum: 357.62ms (81.74ms)

Change tracking fetches, set fetches (25 runs), no caching
------------------------------------------------------------------------------
DataTable, using DbDataAdapter                                       : 594.19ms (20.67ms)   Enum: 77.41ms (4.93ms)
Linq to Sql v4.0.0.0 (v4.6.1586.0)                                   : 706.63ms (30.22ms)   Enum: 3.65ms (0.40ms)
LLBLGen Pro v5.0.0.0 (v5.0.4)                                        : 787.29ms (37.16ms)   Enum: 27.01ms (1.80ms)
Entity Framework v1.0.0.0 (v1.0.0.20622)                             : 1,008.00ms (19.90ms) Enum: 3.76ms (0.34ms)
Oak.DynamicDb using typed dynamic class                              : 1,596.94ms (55.54ms) Enum: 2,268.00ms (101.37ms)
Entity Framework v6.0.0.0 (v6.1.40302.0)                             : 6,363.24ms (76.03ms) Enum: 5.31ms (0.64ms)
NHibernate v4.0.0.4000 (v4.0.4.4000)                                 : 6,473.33ms (128.28ms)    Enum: 5.47ms (1.10ms)

Non-change tracking individual fetches (100 elements, 25 runs), no caching
------------------------------------------------------------------------------
Handcoded materializer using DbDataReader                            : 0.28ms (0.02ms) per individual fetch
Handcoded materializer using DbDataReader (GetValues(array), boxing) : 0.28ms (0.02ms) per individual fetch
Dapper v1.50.0.0                                                     : 0.40ms (0.05ms) per individual fetch
ServiceStack OrmLite v4.0.60.0 (v4.0.60.0)                           : 0.41ms (0.04ms) per individual fetch
Oak.DynamicDb using dynamic Dto class                                : 0.51ms (0.06ms) per individual fetch
Raw DbDataReader materializer using object arrays                    : 0.53ms (0.06ms) per individual fetch
LINQ to DB v1.0.7.3 (v1.0.7.3) (compiled)                            : 0.78ms (0.06ms) per individual fetch
PetaPoco Fast v4.0.3                                                 : 0.82ms (0.04ms) per individual fetch
Massive using dynamic class                                          : 0.82ms (0.14ms) per individual fetch
LINQ to DB v1.0.7.3 (v1.0.7.3) (normal)                              : 0.86ms (0.03ms) per individual fetch
Entity Framework v1.0.0.0 (v1.0.0.20622)                             : 0.89ms (0.04ms) per individual fetch
LLBLGen Pro v5.0.0.0 (v5.0.4), Poco typed view with QuerySpec        : 1.02ms (0.04ms) per individual fetch
LLBLGen Pro v5.0.0.0 (v5.0.4), DataTable based TypedView             : 1.29ms (0.05ms) per individual fetch
Entity Framework v6.0.0.0 (v6.1.40302.0)                             : 1.30ms (0.04ms) per individual fetch
Linq to Sql v4.0.0.0 (v4.6.1586.0)                                   : 3.39ms (0.12ms) per individual fetch
LLBLGen Pro v5.0.0.0 (v5.0.4), Poco typed view with Linq             : 3.58ms (0.09ms) per individual fetch
PetaPoco v4.0.3                                                      : 8.72ms (0.14ms) per individual fetch

Change tracking individual fetches (100 elements, 25 runs), no caching
------------------------------------------------------------------------------
DataTable, using DbDataAdapter                                       : 0.51ms (0.09ms) per individual fetch
Oak.DynamicDb using typed dynamic class                              : 0.58ms (0.04ms) per individual fetch
LLBLGen Pro v5.0.0.0 (v5.0.4)                                        : 0.89ms (0.06ms) per individual fetch
NHibernate v4.0.0.4000 (v4.0.4.4000)                                 : 1.02ms (0.05ms) per individual fetch
Entity Framework v1.0.0.0 (v1.0.0.20622)                             : 1.03ms (0.08ms) per individual fetch
Entity Framework v6.0.0.0 (v6.1.40302.0)                             : 1.47ms (0.04ms) per individual fetch
Linq to Sql v4.0.0.0 (v4.6.1586.0)                                   : 3.64ms (0.13ms) per individual fetch

Change tracking fetches, eager load fetches, 3-node split graph, 1000 root elements (25 runs), no caching
------------------------------------------------------------------------------
Linq to Sql v4.0.0.0 (v4.6.1586.0)                                   : 177.92ms (10.03ms)
LLBLGen Pro v5.0.0.0 (v5.0.4)                                        : 227.49ms (10.30ms)
Entity Framework v1.0.0.0 (v1.0.0.20622)                             : 279.44ms (18.73ms)
NHibernate v4.0.0.4000 (v4.0.4.4000)                                 : 939.34ms (18.69ms)
Entity Framework v6.0.0.0 (v6.1.40302.0)                             : 962.27ms (18.81ms)

Async change tracking fetches, eager load fetches, 3-node split graph, 1000 root elements (25 runs), no caching
------------------------------------------------------------------------------
LLBLGen Pro v5.0.0.0 (v5.0.4)                                        : 226.60ms (8.89ms)
Entity Framework v1.0.0.0 (v1.0.0.20622)                             : 775.05ms (18.05ms)
Entity Framework v6.0.0.0 (v6.1.40302.0)                             : 1,055.69ms (25.01ms)

Change tracking fetches, set fetches (25 runs), caching
------------------------------------------------------------------------------
LLBLGen Pro v5.0.0.0 (v5.0.4)                                        : 360.75ms (139.12ms)  Enum: 27.46ms (4.68ms)

Change tracking individual fetches (100 elements, 25 runs), caching
------------------------------------------------------------------------------
LLBLGen Pro v5.0.0.0 (v5.0.4)                                        : 0.61ms (0.15ms) per individual fetch

分析とパフォーマンスの受け入れ具合は、各システムにより異なると思いますが、Entity Framework Coreが なかなかいい成績だったのではないかと思っています。
また、数年前に比べて Dapper > Entity Framework の差が縮まっているのではないかなぁ・・という印象を持ちました。
加えて重要なのは、このベンチマークはクエリーのみで更新系は測定されていません。Entity Frameworkは特に更新系パフォーマンスに懸念点が多く挙げられていたので、最新版で改めて別途ベンチマーク測定を行ってみようとは思っています。

次回のブログ更新は、Entity Framework Coreにも対応している Dapper をいじってみようかと思います。