1つのIoCコンテナーの最適化の歴史

この記事では、小さいながらも非常に有用なプロジェクトについて情報を共有したいと思います。StefánJökullSigurðarsonは、.NET Coreに移行したことがわかっているすべてのIoCコンテナーを追加し、 BenchmarkDotNetを使用したインスタンス測定解決を使用しますパフォーマンス。 私はこのコンペティションに参加する機会を逃しませんでした 。私は自分の小さなプロジェクトFsContainerに参加しました







画像







1.2.0



プロジェクトが.NET Coreに移行された後(私はそれが絶対に難しくないことが判明したことに注意したい)、私は失望しなかったと言って、それは何も言わないことを意味し、これは3つの測定値の1つが私のコンテナーを通過しなかったという事実によるものでした。 この言葉の直接的な意味では、測定は単純に20分以上続き、終了しませんでした。







その理由は次のコードセクションにあります。







public object Resolve(Type type) { var instance = _bindingResolver.Resolve(this, GetBindings(), type); if (!_disposeManager.Contains(instance)) { _disposeManager.Add(instance); } return instance; }
      
      





考えてみると、ベンチマーク操作の主な原則は、単位時間(オプションのメモリ消費量)ごとに実行される操作の数を測定することです。つまり、 Resolve



メソッドはできるだけ多く実行されます。 container.Dispose()



場合、解決後、結果のインスタンスが_disposeManager



に追加されてさらに破棄されることに気付くかもしれません。 なぜなら 実装の内部にはList<object>



、そのインスタンスはContains



チェックすることで追加さContains



、すぐに2つの副作用があると推測できContains









  1. Contains



    チェックを使用して新しく作成された各インスタンスは、 GetHashCode



    を計算し、以前に追加されたインスタンスの重複を探します。
  2. なぜなら 新しく作成された各インスタンスは常に一意になり(解決はTransientLifetimeManager



    でテストされました)、 List<object>



    サイズは、メモリの新しい2倍の部分を割り当て、 以前に追加された要素をコピーして (メモリ割り当て操作の100万インスタンスを追加するため)常に増加しますコピーは少なくとも20回呼び出されます);


率直に言って、この場合、どのソリューションが最も正しいかわかりません。実際には、1つのコンテナが以前に作成されたインスタンスへの数百万のリンクを保持することを想像するのは難しいため、(かなり論理的な)制限を追加することで問題の半分しか解決しませんでしたIDisposable



を実装するオブジェクトのみを_disposeManager



追加し_disposeManager









 if (instance is IDisposable && !_disposeManager.Contains(instance)) { _disposeManager.Add(instance); }
      
      





その結果、測定はかなり許容できる時間で完了し、次の結果が得られました。







方法 平均 エラー Stddev スケーリング済み ScaledSD Gen 0 Gen 1 割り当て済み
直接 13.77 ns 0.3559 ns 0.3655 ns 1.00 0.00 0.0178 - 56 B
ライトインジェクト 36.95 ns 0.1081 ns 0.0902 ns 2.69 0.07 0.0178 - 56 B
シンプルインジェクター 46.17 ns 0.2746 ns 0.2434 ns 3.35 0.09 0.0178 - 56 B
アスプネットコア 71.09 ns 0.4592 ns 0.4296 ns 5.17 0.14 0.0178 - 56 B
Autofac 1,600.67 ns 14.4742 ns 12.8310 ns 116.32 3.10 0.5741 - 1803 B
構造図 1,815.87 ns 18.2271 ns 16.1578 ns 131.95 3.55 0.6294 - 1978 B
Fscontainer 2,819.01 ns 6.0161 ns 5.3331 ns 204.85 5.24 0.4845 - 1524 B
Ninject 12,812.70 ns 255.5191 ns 447.5211 ns 931.06 39.95 1.7853 0.4425 5767 B


もちろん、私はそれらに満足しておらず、さらなる最適化方法を探し始めました。







1.2.1



コンテナの現在のバージョンでは、必要なコンストラクターとそれに必要な引数の定義は変更されていないため、この情報をキャッシュでき、今後CPU時間を浪費することはありません。 この最適化の結果、 ConcurrentDictionary



が追加され、そのキーは要求されたタイプ( Resolve<T>



)であり、値は直接インスタンス化するために使用されるコンストラクターと引数です。







 private readonly IDictionary<Type, Tuple<ConstructorInfo, ParameterInfo[]>> _ctorCache = new ConcurrentDictionary<Type, Tuple<ConstructorInfo, ParameterInfo[]>>();
      
      





行われた測定から判断すると、このような単純な操作により、生産性が30%以上向上しました。







方法 平均 エラー Stddev スケーリング済み ScaledSD Gen 0 Gen 1 Gen 2 割り当て済み
直接 13.50 ns 0.2240 ns 0.1986 ns 1.00 0.00 0.0178 - - 56 B
ライトインジェクト 36.94 ns 0.0999 ns 0.0886 ns 2.74 0.04 0.0178 - - 56 B
シンプルインジェクター 46.40 ns 0.3409 ns 0.3189 ns 3.44 0.05 0.0178 - - 56 B
アスプネットコア 70.26 ns 0.4897 ns 0.4581 ns 5.21 0.08 0.0178 - - 56 B
Autofac 1,634.89 ns 15.3160 ns 14.3266 ns 121.14 2.01 0.5741 - - 1803 B
Fscontainer 1,779.12 ns 18.9507 ns 17.7265 ns 131.83 2.27 0.2441 - - 774 B
構造図 1,830.01 ns 5.4174 ns 4.8024 ns 135.60 1.97 0.6294 - - 1978 B
Ninject 12,558.59 ns 268.1920 ns 490.4042 ns 930.58 38.29 1.7858 0.4423 0.0005 5662 B


1.2.2



測定を行うとき、BenchmarkDotNetは、アセンブリが最適化されていない可能性があることをユーザーに通知します(デバッグ構成でアセンブル)。 長い間、このメッセージがnugetパッケージを使用してコンテナに接続されているプロジェクトでこのメッセージが表示された理由と、nugetパックのパラメーターの可能なリストを見たときの驚きは理解できませんでした:







 nuget pack MyProject.csproj -properties Configuration=Release
      
      





デバッグ構成でパッケージを収集していた間、更新された測定結果から判断すると、パフォーマンスが最大で25%低下したことがわかりました。







方法 平均 エラー Stddev スケーリング済み ScaledSD Gen 0 Gen 1 Gen 2 割り当て済み
直接 13.38 ns 0.2216 ns 0.2073 ns 1.00 0.00 0.0178 - - 56 B
ライトインジェクト 36.85 ns 0.0577 ns 0.0511 ns 2.75 0.04 0.0178 - - 56 B
シンプルインジェクター 46.56 ns 0.5329 ns 0.4724 ns 3.48 0.06 0.0178 - - 56 B
アスプネットコア 70.17 ns 0.1403 ns 0.1312 ns 5.25 0.08 0.0178 - - 56 B
Fscontainer 1,271.81 ns 4.0828 ns 3.8190 ns 09.09 1.44 0.2460 - - 774 B
Autofac 1,648.52 ns 2.3197 ns 2.0563 ns 123.26 1.84 0.5741 - - 1803 B
構造マップ 1,829.05 ns 17.8238 ns 16.6724 ns 136.75 2.37 0.6294 - - 1978 B
Ninject 12,520.08 ns 248.2530 ns 534.3907 ns 936.10 41.98 1.7860 0.4423 0.0008 5662 B


1.2.3



もう1つの最適化は、アクティベーター関数のキャッシュで、Expressionを使用してコンパイルされます。







 private readonly IDictionary<Type, Func<object[], object>> _activatorCache = new ConcurrentDictionary<Type, Func<object[], object>>();
      
      





汎用関数は、引数ConstructorInfo



と引数ParameterInfo[]



配列を受け取り、結果として厳密に型指定されたラムダを返します。







 private Func<object[], object> GetActivator(ConstructorInfo ctor, ParameterInfo[] parameters) { var p = Expression.Parameter(typeof(object[]), "args"); var args = new Expression[parameters.Length]; for (var i = 0; i < parameters.Length; i++) { var a = Expression.ArrayAccess(p, Expression.Constant(i)); args[i] = Expression.Convert(a, parameters[i].ParameterType); } var b = Expression.New(ctor, args); var l = Expression.Lambda<Func<object[], object>>(b, p); return l.Compile(); }
      
      





この解決策の論理的な継続は、ActivatorだけでなくResolve関数全体をコンパイルすることであることに同意します







方法 平均 エラー Stddev スケーリング済み ScaledSD Gen 0 Gen 1 Gen 2 割り当て済み
直接 13.24 ns 0.0836 ns 0.0698 ns 1.00 0.00 0.0178 - - 56 B
ライトインジェクト 37.39 ns 0.0570 ns 0.0533 ns 2.82 0.01 0.0178 - - 56 B
シンプルインジェクター 46.22 ns 0.2327 ns 0.2063 ns 3.49 0.02 0.0178 - - 56 B
アスプネットコア 70.53 ns 0.2885 ns 0.2698 ns 5.33 0.03 0.0178 - - 56 B
Fscontainer 1,038.13 ns 17.1037 ns 15.9988 ns 78.41 1.23 0.2327 - - 734 B
Autofac 1,551.33 ns 3.6293 ns 3.2173 ns 117.17 0.64 0.5741 - - 1803 B
構造マップ 1,944.35 ns 1.8665 ns 1.7459 ns 146.85 0.76 0.6294 - - 1978 B
Ninject 13,139.70 ns 260.8754 ns 508.8174 ns 992.43 38.35 1.7857 0.4425 0.0004 5682 B


1.2.4



記事の公開後、 @ turbanoffは、 ConcurrentDictionary



の場合、 GetOrAdd



メソッドのパフォーマンスがContainsKey / Add ConcurrentDictionary



パフォーマンスよりも高いことにGetOrAdd



ました。 測定結果を以下に示します。







宛先:







 if (!_activatorCache.ContainsKey(concrete)) { _activatorCache[concrete] = GetActivator(ctor, parameters); }
      
      





方法 平均 エラー Stddev 中央値 Gen 0 割り当て済み
シングルトンを解決する 299.0 ns 7.239 ns 19.45 ns 295.7 ns 0.1268 199 B
ResolveTransient 686.3 ns 32.333 ns 86.30 ns 668.7 ns 0.2079 327 B
ResolveCombined 1,487.4 ns 101.057 ns 273.21 ns 1,388.7 ns 0.4673 734 B


後:







 var activator = _activatorCache.GetOrAdd(concrete, x => GetActivator(ctor, parameters));
      
      





方法 平均 エラー Stddev Gen 0 割り当て済み
シングルトンを解決する 266.6 ns 4.955 ns 4.393 ns 0.1268 199 B
ResolveTransient 512.0 ns 16.974 ns 16.671 ns 0.3252 511 B
ResolveCombined 1,119.2 ns 18.218 ns 15.213 ns 0.6943 1101 B


PS



実験として、さまざまなデザインを使用してオブジェクトの作成時間を測定することにしました。 プロジェクト自体はGithubで入手でき、以下の結果を見ることができます。 完全を期すために、欠落しているのは、Directメソッドに可能な限り近いIL命令を生成するアクティベーションメソッドです。これは、トップ4のコンテナーで使用されるメソッドであり、このような印象的な結果を達成できます







方法 平均 エラー Stddev Gen 0 割り当て済み
直接 4.031 ns 0.1588 ns 0.1890 ns 0.0076 24 B
Compiledinvoke 85.541 ns 0.5319 ns 0.4715 ns 0.0178 56 B
ConstructorInfoInvoke 316.088 ns 1.8337 ns 1.6256 ns 0.0277 88 B
ActivatorCreateInstance 727.547 ns 2.9228 ns 2.5910 ns 0.1316 416 B
Dynamicinvoke 974.699 ns 5.5867 ns 5.2258 ns 0.0515 168 B



All Articles