パフォーマンス徹底比較 Seasar2 vs Spring 2006/04/12 株式会社電通国際情報サービスひがやすを株式会社アークシステム本間宏崇 1
目的 DI コンテナの実装によるパフォーマンスの違いを明らかにする DI コンテナが行う処理の中で どこに時間が掛かるのかを明らかにする 2
ベンチマーク測定環境 ハードウェア HP ProLiant DL360 G4p CPU: Intel Xeon 3.80GHz (2 CPU) Memory: 4GB ソフトウェア OS: Red Hat Enterprise Linux AS 4 Update 3 (x86) Java: 1.5.0_06 (Sun) 3
DI コンテナ Seasar 2.4 beta1 (2006/03/27) Spring 2.0 M3 (2006/03/08) ベンチマークプログラム 自作 測定アプリケーション 4
VM オプション -Xmx1024M -Xms1024M -XX:PermSize=384M -XX:MaxPermSize=384M fork=true JVM のキャッシュをクリアするため 測定方法 5 回実行し 最大 最小を除いた 3 回の平均値を採る 5
DI コンテナがやっていること コンテナ生成 XML 読み込み (DOM や SAX) 定義からコンポーネントを組み立てる DI リフレクション リフレクション情報を取得しキャッシュする リフレクションを使用しプロパティへアクセスする AOP バイトコード作成 6
それぞれの処理について パフォーマンスを見ていきましょう まずは XML 読み込みを行っている コンテナ生成処理からです 7
コンテナ生成 コンテナ生成時の内部処理 Seasar SAX リフレクション情報をキャッシュ Spring DOM リフレクション処理はここでは行わない 8
コンテナへ入力する設定ファイル Seasar <components> <component name="nullbean00000" class="xxx.nullbean00000" /> <component name="nullbean00001" class="xxx.nullbean00001" /> Spring <beans> <bean name="nullbean00000" class= xxx.nullbean00000" /> <bean name="nullbean00001" class= xxx.nullbean00001" /> 9
コンテナ生成 コンテナ生成処理 5000 4500 4000 3500 3000 ミリ秒 2500 2000 1500 Seasar Spring 1000 500 0 1000 2000 5000 10000 コンポーネント数 10
コンテナ生成 : 結果 Seasar Spring 理由 リフレクション情報をキャッシュするぶん Seasar の方が多くの処理を行っていますが SAX と DOM の性能差によって吸収されていると思われます 11
コンポーネント取得 次は 生成したコンテナからコンポーネントを取得する処理です コンテナに登録されている全てのコンポーネントを取得するのに掛かった時間を計測しました DI AOP は使用していません 単純に コンテナからコンポーネントを取得するのみです 12
コンポーネント取得 12000 10000 8000 ミリ秒 6000 4000 Seasar Spring 2000 0 1000 2000 5000 10000 コンポーネント数 13
コンポーネント取得 : 結果 Seasar >>(10~30 倍 )>> Spring 1000 個で 1400ms コンポーネントを生成するという点ではどちらも一緒のはずですが どうして差が出るのでしょうか? 14
コンポーネント取得 理由 DI コンテナは コンポーネントを生成するためにリフレクション情報を使用しています Seasar はコンテナ生成時にリフレクション情報をキャッシュしています コンポーネント生成時にはキャッシュした情報を使用しています Spring はコンポーネントを取得するときにリフレクション情報をキャッシュしています 15
コンポーネント取得 理由 Spring はコンポーネント取得時にリフレクション処理を行っているため 遅くなります Seasar はコンテナ生成時にリフレクション処理を行っていますが SAX と DOM の性能差によって Spring との差が無くなっています そのため コンポーネント取得時に Seasar の速さが際立っています 16
リフレクション処理 では Seasar と Spring のリフレクション処理はどれくらい違うのでしょうか? リフレクション処理を行うクラスを直接呼び出して測定しました Seasar: BeanDescImpl Spring: BeanWrapperImpl 17
リフレクション処理 リフレクション情報をキャッシュ 14000 12000 10000 ミリ秒 8000 6000 4000 Seasar Spring 2000 0 1000 2000 5000 10000 コンポーネント数 18
リフレクション処理 : 結果 Seasar >(3 倍 )> Spring 1000 回で 1300ms 理由 Seasar はリフレクションキャッシュ処理を独自で実装しています Spring の BeanWrapperImpl は JDK の Introspector を使用しています この違いが速度差となっていると思われます キャッシュ実装の違い Seasar: HashMap Spring: WeakHashMap 19
Seasar のコンテナ init 処理 Seasar ではコンテナ生成直後に init 処理を行うことができます 先ほどまではコンテナの init 処理を行っていませんでした init 処理を行わない場合は 1 度目にコンポーネントを取得したタイミングで そのコンポーネントがインスタンス化されます init 処理では singleton のコンポーネントを作成することができます singleton とは コンポーネント生成は最初 1 度だけで その後は最初に生成したコンポーネントを返すこと 20
Seasar のコンテナ init 処理 実際の案件では アプリケーション起動時に init 処理で singleton のコンポーネントを生成した方が効率的です Spring にはこのような機能はありません init 処理を含めた場合のコンテナ生成でのパフォーマンスを見てみましょう Seasar: コンテナ生成 + init Spring: コンテナ生成 21
Seasar のコンテナ init 処理 コンテナ生成 ( + init 処理 ) 6000 5000 4000 ミリ秒 3000 2000 Seasar Spring 1000 0 1000 2000 5000 10000 コンポーネント数 22
Seasar Spring Seasar のコンテナ init 処理 : 結果 理由 init 処理では singleton のオブジェクトを生成しているだけなので それほど時間が掛かりません コンテナ作成時の速度は Seasar の方が速いため init でのオーバーヘッドをカバーできます 23
では... create + init した場合での コンポーネント取得パフォーマンスは? 24
create + init した後のコンポーネント取得処理 14000 12000 10000 ミリ秒 8000 6000 4000 Seasar Spring 2000 0 1000 2000 5000 10000 コンポーネント数 25
結果 Seasar >>>>>>> (60~200 倍 ) >> >>>>>>>>>>> Spring 1000 個で 1500ms 実際の案件ではアプリケーション初期化時に create + init 処理を行っているので これが現実のプロジェクトで起こる結果を反映しています ただし コンテナから 2 回目に取り出すときは Seasar も Spring もキャッシュしたオブジェクトを返すだけなので 差は付きません 26
prototype 今までは singleton の場合でした 今度は prototype の場合を見てみましょう prototype とは コンポーネントを取得するたびに新たに生成することです prototype でも 1 度目の取得は singleton と同様に圧倒的な差が出ます 2 度目の取得で比べてみましょう 27
prototype prototype で 2 度目のコンポーネント取得 900 800 700 600 ミリ秒 500 400 300 200 Seasar Spring 100 0 1000 2000 5000 10000 コンポーネント数 28
prototype: 結果 Seasar >(3~5 倍 )> Spring 1000 個で 130ms 理由 Spring では対象となるオブジェクトに加えて BeanWrapperImpl を毎回作っていますが Seasar では対象となるオブジェクトしか作りません これが原因の 1 つかもしれません 29
DI (Manual) 次は DI 処理について見てみましょう DI とは あるコンポーネントが必要とする他のコンポーネントを コンテナがセットしてあげることです ( ざっくり ) 現実的な状況を反映させるため 最初にコンテナ生成と init を実行した上で比較しています コンテナへコンポーネントを 2000 個登録しています 2 個で 1 組です 30
DI (Manual) コンテナ生成 (Seasar は init を含む ) 3500 3000 2500 2000 ミリ秒 1500 1000 Seasar Spring 500 0 31
DI (Manual): 結果 Seasar < Spring 差は 600ms 前回コンテナ生成を比較した場合はほぼ一緒でしたが... 32
DI (Manual): 結果 理由 今回 2000 個のコンポーネントでコンテナ生成した場合は 600ms 差が出ています この差はリフレクションキャッシュによるものです 前回より 1000 個余分にキャッシュしていることが今回の 600ms の差につながっています Seasar でリフレクションキャッシュ 1000 個と 2000 個を作成する時間の差が 400ms でしたので 若干違いますがほぼその差と思われます 差が大きくないのと 初期化時の処理であることを考えると 現実にはあまり問題にならないと思います 33
DI (Manual) 今度は実際にユーザに影響する部分である DI したコンポーネントを取得する処理を見てみましょう 34
DI (Manual) DI したコンポーネントを取得 (1000 個 ) Manual DI singleton 3000 2500 2000 ミリ秒 1500 1000 500 Seasar Spring 0 35
DI (Manual): 結果 Seasar >>>>>> (100 倍 ) >>>>> >>>>>>>>> Spring 1000 セットで 2400ms DI 無しの場合と比べると... 今回は 2000 個から 1000 個取り出していますが 1000 個から 1000 個取り出すのと速度は同じですので そのときと比べてみましょう DI 無しの場合は 60 倍 今回は 100 倍 DI することによってさらに差が開いています 36
DI (Manual): 結果 理由 DI するときに プロパティに対してリフレクションでアクセスしています リフレクションを行うクラスの性能差が一因と思われます リフレクションでのアクセスがどれくらいか見てみましょう 1 プロパティへ set, get して測定しました Seasar: BeanDescImpl Spring: BeanWrapperImpl 37
リフレクション リフレクションでのプロパティアクセス 600 500 400 ミリ秒 300 200 Seasar Spring 100 0 1000 2000 5000 10000 回数 (set,get で 1 回 ) 38
Seasar > (4~8 倍 ) > Spring 1000 回で 100ms リフレクション : 結果 理由 BeanDescImpl と BeanWrapperImpl の差と思われます BeanWrapperImpl ではネストしたプロパティをサポートしており それ関連のオーバーヘッド ( 文字列操作とか ) が大きいと思われます 39
次は prototype で明示的に DI を指定した場合の 2 度目のアクセスについてです 40
DI したコンポーネントを取得 (1000 個 ) Manual DI prototype 250 200 150 ミリ秒 100 50 Seasar Spring 0 41
結果 Seasar >(3 倍 )> Spring 1000 セットで 150ms DI しない場合でも prototype での 2 度目の取得は 3~5 倍の差だったので DI 処理のぶん更に差が出ると思いましたが 想定したほどではありませんでした 42
autowire 設定ファイルを少しでも少なくするために autowire というものがあります 設定ファイルに DI を指定する property タグを書かなくて良くなります autowire には幾つか種類がありますが ここでは型による DI を使用しています 43
autowire DI したコンポーネントを取得 (1000 個 ) autowire bytype singleton 7000 6000 5000 4000 ミリ秒 3000 2000 Seasar Spring 1000 0 44
autowire: 結果 Seasar >>>>>>>>>>>>>>> >>>>>>>>>>(300 倍 )>>>>> >>>>>> Spring 1000 セットで 6000ms Manual では 100 倍の差でしたが Auto にすると更に 3 倍の差が付きました 45
autowire: 結果 理由 autowire 時には DI 対象を探すロジックが実行されます Spring では DI の度に 毎回コンテナへ登録されている全てのオブジェクトへアクセスします コンテナには 2000 個登録されていて 1000 回 DI しているので 2000 * 1000 回コンポーネント定義へアクセスしています Seasar はコンポーネントを登録するときにクラスの型をキー情報としてハッシュテーブルへ登録しているので DI の度に 1 回のアクセスで済みます つまり DI の度に List へ全件アクセスするのか HashMap へキーでアクセスするのかの差なので 差が付いて当たり前と言えるでしょう 46
autowire で prototype の場合はどうでしょうか? 2 回目のコンポーネント取得時 47
DI したコンポーネントを取得 (1000 個 ) autowire bytype prototype 3000 2500 2000 ミリ秒 1500 1000 500 Seasar Spring 0 48
結果 Seasar >>>> (35 倍 ) >>>>> Spring 1000 セットで 2300ms 理由 singleton と同じで DI 対象を探すロジックの差でしょう singleton ほどではありませんが 大きな差が出ました 49
AOP AOP とは バイトコードを操作しもともとの処理をカスタマイズするもの ( ざっくり ) AOP を掛けたメソッドを実行して 速度差を見てみましょう 今回の AOP は文字列を返すだけの 非常にシンプルなものです だからこそ AOP のオーバーヘッドがわかりやすいと思います 10,000,000 回メソッドを実行 Seasar は Javassist Spring は CGLIB (DynamicProxy よりも速い ) 50
AOP AOP を仕掛けたメソッドを実行 3500 3000 2500 2000 ミリ秒 1500 1000 Seasar Spring 500 0 51
AOP: 結果 Seasar >(3~4 倍 )> Spring 10,000,000 回で 2400ms 理由 Seasar は 2.1 までは CGLIB で 2.2 からは Javassist に変えて 約 3 倍速くなったことがあります CGLIB を使うと殆どチューニングの余地がありませんが Javassist にはチューニングの余地があります Seasar ではかなりのチューニングを行っているので 速くなっていると思われます 52
AOP weaving AOP を組み込むバイトコード操作を weaving と呼んでいます この weaving もパフォーマンスに与える影響があると考えたため 測定してみました 53
AOP weaving まずは weaving するクラスを直接呼び出して 速度差を比較しました Seasar: AopProxy Spring: ProxyFactory 54
AOP weaving AOP の Weaving 140000 120000 100000 ミリ秒 80000 60000 40000 Seasar Spring 20000 0 1000 2000 5000 10000 回数 55
AOP weaving: 結果 Seasar >(3 倍 )> Spring 1000 回で 8000ms 理由 Javassist と CGLIB でのバイトコード weaving の速度差と思われます AOP の weaving にかかる絶対時間が大きいことがわかります (1000 個で 8 秒!) 56
次は 登録されているコンポーネントへまとめて Aspect を仕掛けて コンテナを生成してみます まとめて Aspect を仕掛ける機能 Seasar: AspectAutoRegister Spring: AutoProxyCreator これらを使ってみました 57
AOP 自動登録でのコンテナ生成 300000 250000 200000 ミリ秒 150000 100000 Seasar Spring 50000 0 1000 2000 5000 10000 回数 58
結果 Seasar >>> (15~60 倍 ) > > > Spring 1000 個で 15000ms 理由 リフレクション情報のキャッシュ AOP weaving やはり AOP weaving は DI コンテナの処理の中では重い部類に入ることがわかります 59
補足情報 Spring は ( 今回使用した方法で )AOP を登録すると コンテナ生成時にリフレクション情報をキャッシュしコンポーネントを生成するようです 1 度目のコンポーネント取得時に発生していた負荷がコンテナ生成時に寄っています そのぶん コンポーネント取得時の速度は Seasar と同じくらいに速くなっています 60
まとめ DI という同じ技術を実装してこれほどの差が出るのはかなり驚きです ある程度 原因も指摘しているので この結果を元に Spring のチューニングに役立ててもらえれば幸いです この結果およびテストプログラムはオープンソースとして公開する予定です 61
本日はご静聴いただき ありがとうございました 62