みなさんActivatorUtilities.CreateFactoryをご存知でしょうか。ActivatorUtilities.CreateInstanceと名前が似ているので見落としていたという人もいるのではないでしょうか。このメソッドはActivatorUtilities.CreateInstanceを大量に呼び出す画面で役立ちます。
最近私はServer/Clientフレームワークを作っているのですが、接続毎にDIで接続オブジェクトをDIでインスタンス化したいという要求がありました。具体的には以下のような処理です。
var acceptSocket = await listenSocket.AcceptAsync(cancellationToken);
var connection = ActivatorUtilities.CreateInstance<Connection>(services, acceptSocket);
しかしシステムによっては何万もの大量の接続が発生する場合もあります。DIによるインスタンス化はActivatorUtilities.CreateInstanceを使うのですが、これはリフレクションを使いコンストラクタを探索しているため性能面で懸念がありました。
これを解決する方法を調べる中でActivatorUtilities.CreateFactoryというものがあることを知りました。これを使えば毎回リフレクションを使うことを避けることができます。
以下に詳しく紹介しましょう。
ActivatorUtilities.CreateInstanceとは
まずは ActivatorUtilities.CreateInstanceはみなさんご存知かと思いますがDIによるインスタンス生成を行います。
T CreateInstance<T>(IServiceProvider provider, params object[] parameters);
このメソッドはIServiceProvider(provider)と、任意の直接引数(parameters)を指定してインスタンスを生成します。DI向けのクラスを動的にインスタンス化したい場面でとても便利なメソッドですね。このメソッドは大きく以下の3つのことを行っています。
- コンストラクタの探索
リフレクションを使いコンストラクタを探す - 依存サービスの解決
- 依存サービスと直接引数とともにコンストラクタを呼ぶ
上記のうちコンストラクタの探索と依存サービスの解決の実行コストが大きいです。
ActivatorUtilities.CreateFactoryとは
ActivatorUtilities.CreateFactoryの機能はリファレンスによると以下になります。
IServiceProviderから直接またはそこから提供されるコンストラクター引数を使用して型をインスタンス化するデリゲートを作成します。
つまりCreateInstanceで行っているコンストラクタの探索を行いデリゲート化してくれるわけですね。
デリゲート化したコンストラクタを使い回せば性能面で有利になりそうですね。
ベンチマーク
ではCreateFactoryを使うことでどれくらい性能が上がるのか、検証してみましょう。
インスタンス化するクラスはいくつかの依存サービスと直接引数があるコンストラクタを持ちます。
class ClassA(ILogger<ClassA> logger, string message, Stopwatch sw, int no)
{
}
このテストクラスを複数回インスタンス化する処理を、以下の3つのインスタンス化手法で比較しました。
- UseNew
new構文を使ってインスタンス化します。もちろんこれが一番早くなるはずですが、要件により使えません。今回は参考値として追加しています。 - UseCreateInstance
ActivatorUtilities.CreateInstanceを使います。毎回リフレクションを使うので一番遅くなるはずです。 - UseCreateFactory
最初にActivatorUtilities.CreateFactoryでデリゲート化し、それを使いまわします。インスタンス化数が多くなるにしたがって早くなるはずです。
検証コードは以下にあります。
ベンチマーク結果
ベンチマーク結果は以下のとおりです。
| Method | CreateCount | Mean | Error | StdDev | Median | Ratio |
|---|---|---|---|---|---|---|
| UseNew | 1 | 10.49 us | 0.204 us | 0.243 us | 10.55 us | 0.96 |
| UseCreateInstance | 1 | 10.90 us | 0.192 us | 0.235 us | 10.88 us | 1.00 |
| UseCreateFactory | 1 | 407.03 us | 4.701 us | 4.397 us | 406.15 us | 37.37 |
| UseNew | 1000 | 14.51 us | 0.167 us | 0.148 us | 14.50 us | 0.05 |
| UseCreateInstance | 1000 | 300.63 us | 2.557 us | 2.843 us | 300.85 us | 1.00 |
| UseCreateFactory | 1000 | 725.57 us | 23.186 us | 65.775 us | 747.93 us | 2.41 |
| UseNew | 5000 | 31.06 us | 0.368 us | 0.344 us | 31.07 us | 0.02 |
| UseCreateInstance | 5000 | 1,350.49 us | 12.421 us | 11.619 us | 1,353.84 us | 1.00 |
| UseCreateFactory | 5000 | 693.87 us | 13.064 us | 10.909 us | 696.99 us | 0.51 |
※us = Microsecond (0.000001 sec)
ベンチマーク結果からわかるのは、
- newはインスタンス生成回数に関わらず最も早い。UseCreateFactoryとの差は依存サービスの解決を毎回行っていない点
- UseCreateFactoryは初回のデリゲート生成があるため、1回のインスタンス生成では他より約40倍遅く、約0.0004秒掛かる
- 5000回のインスタンス生成ではUseCreateFactoryがUseCreateInstanceを逆転する。デリゲート生成コストの損益分岐点は5000回程度
まとめ
ベンチマーク結果からどの方法も分散して発生するのであれば体感できる違いはないということです。しかし逆に言うと短時間に大量に発生する場合は体感できる違いになってきます。
例えばHTTPアクセスが集中し短時間に5000リクエスト来た場合、CreateFactoryを使えば約3.5秒、使わなければ約6.75秒がインスタンス生成部分において掛かるという違いがでるでしょう。リクエスト処理はインスタンス生成部分だけではないので処理が逼迫する状況では可能な限り性能に気を使いたいですね。また依存サービスの解決部分も使いまわしができればUseNewと同等(0.15秒に)になるので将来の機能改善に期待します。
結論としてはCreateFactoryの使い所は 「短時間に5000回以上インスタンス化する可能性がある場合」 になると思います。逆に言うとそれ以外の場面ではあまり気にする必要はないでしょう。
これまでActivatorUtilities.CreateInstanceは重いのでなんとなく避けなければならないと思っていましたが、低頻度であれば問題ないと分かったのでこれからは積極的に使っていこうと思います。