この記事では、小さいながらも非常に有用なプロジェクトについて情報を共有したいと思います。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
:
-
Contains
チェックを使用して新しく作成された各インスタンスは、GetHashCode
を計算し、以前に追加されたインスタンスの重複を探します。 - なぜなら 新しく作成された各インスタンスは常に一意になり(解決は
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 |