ObjectPool は、Roslyn C#コンパイラで使用されるタイプで、通常は新しくなり、ガベージコレクションが頻繁に行われる頻繁に使用されるオブジェクトを再利用します。これにより、発生する必要のあるガベージコレクション操作の量とサイズが削減されます。
Roslynコンパイラには、オブジェクトのいくつかの個別のプールがあるようで、各プールのサイズは異なります。なぜこれほど多くの実装があるのか、好ましい実装は何か、そしてなぜ彼らが20、100、または128のプールサイズを選んだのかを知りたいです。
1 - SharedPools -20個のオブジェクトのプールを格納します。BigDefaultが使用されている場合は100個を格納します。これは、PooledObjectの新しいインスタンスを作成するという点でも奇妙です。これは、オブジェクトをプールしようとしていて、新しいオブジェクトを作成および破棄しない場合は意味がありません。
// Example 1 - In a using statement, so the object gets freed at the end.
using (PooledObject<Foo> pooledObject = SharedPools.Default<List<Foo>>().GetPooledObject())
{
// Do something with pooledObject.Object
}
// Example 2 - No using statement so you need to be sure no exceptions are not thrown.
List<Foo> list = SharedPools.Default<List<Foo>>().AllocateAndClear();
// Do something with list
SharedPools.Default<List<Foo>>().Free(list);
// Example 3 - I have also seen this variation of the above pattern, which ends up the same as Example 1, except Example 1 seems to create a new instance of the IDisposable [PooledObject<T>][3] object. This is probably the preferred option if you want fewer GC's.
List<Foo> list = SharedPools.Default<List<Foo>>().AllocateAndClear();
try
{
// Do something with list
}
finally
{
SharedPools.Default<List<Foo>>().Free(list);
}
2 -- ListPool および StringBuilderPool -厳密に分離された実装ではなく、上記のSharedPools実装のラッパーで、特にListおよびStringBuilder用です。したがって、これはSharedPoolsに格納されているオブジェクトのプールを再利用します。
// Example 1 - No using statement so you need to be sure no exceptions are thrown.
StringBuilder stringBuilder= StringBuilderPool.Allocate();
// Do something with stringBuilder
StringBuilderPool.Free(stringBuilder);
// Example 2 - Safer version of Example 1.
StringBuilder stringBuilder= StringBuilderPool.Allocate();
try
{
// Do something with stringBuilder
}
finally
{
StringBuilderPool.Free(stringBuilder);
}
3 - PooledDictionary および PooledHashSet -これらはObjectPoolを直接使用し、オブジェクトの完全に別個のプールを持っています。 128個のオブジェクトのプールを格納します。
// Example 1
PooledHashSet<Foo> hashSet = PooledHashSet<Foo>.GetInstance()
// Do something with hashSet.
hashSet.Free();
// Example 2 - Safer version of Example 1.
PooledHashSet<Foo> hashSet = PooledHashSet<Foo>.GetInstance()
try
{
// Do something with hashSet.
}
finally
{
hashSet.Free();
}
.NETCoreには新しいオブジェクトプーリングの実装があります。 C#オブジェクトプーリングパターンの実装 の質問に対する私の答えを参照してください。
私はRoslynパフォーマンスvチームのリーダーです。すべてのオブジェクトプールは、割り当て率、つまりガベージコレクションの頻度を減らすように設計されています。これには、長寿命(第2世代)のオブジェクトを追加するという犠牲が伴います。これはコンパイラのスループットをわずかに向上させますが、主な影響はVBまたはC#IntelliSenseを使用する場合のVisualStudioの応答性にあります。
なぜこれほど多くの実装があるのか」。
簡単な答えはありませんが、次の3つの理由が考えられます。
推奨される実装は何ですか
_ObjectPool<T>
_が推奨される実装であり、コードの大部分が使用するものです。 _ObjectPool<T>
_はArrayBuilder<T>.GetInstance()
によって使用され、おそらくRoslynでプールされたオブジェクトの最大のユーザーであることに注意してください。 _ObjectPool<T>
_は非常に頻繁に使用されるため、これは、リンクされたファイルを介してレイヤー間でコードを複製したケースの1つです。 _ObjectPool<T>
_は、最大スループットになるように調整されています。
ワークスペースレイヤーでは、_SharedPool<T>
_が、プールされたインスタンスを互いに素なコンポーネント間で共有して、全体的なメモリ使用量を削減しようとしていることがわかります。各コンポーネントが特定の目的専用の独自のプールを作成することを避け、代わりに要素のタイプに基づいて共有しようとしていました。この良い例はStringBuilderPool
です。
彼らが20、100または128のプールサイズを選んだ理由。
通常、これは一般的なワークロードでのプロファイリングとインストルメンテーションの結果です。通常、割り当て率(プール内の「ミス」)とプール内の合計ライブバイト数のバランスをとる必要があります。関係する2つの要因は次のとおりです。
物事の壮大なスキームでは、プール内のオブジェクトによって保持されるメモリは、コンパイルの合計ライブメモリ(Gen 2ヒープのサイズ)と比較して非常に小さいですが、巨大なオブジェクト(通常は大きい)を返さないようにも注意しますコレクション)プールに戻る-ForgetTrackedObject
を呼び出して、それらを床にドロップします
将来的には、改善できる領域の1つは、長さが制限されたバイト配列(バッファー)のプールを用意することだと思います。これは、特に、コンパイラの発行フェーズ(PEWriter)でのMemoryStreamの実装に役立ちます。これらのMemoryStreamは、高速書き込みのために連続したバイト配列を必要としますが、動的にサイズ設定されます。つまり、サイズを変更する必要がある場合があります。通常、サイズは毎回2倍になります。各サイズ変更は新しい割り当てですが、サイズ変更されたバッファーを専用プールから取得して、小さい方のバッファーを別のプールに戻すことができれば便利です。したがって、たとえば、64バイトのバッファ用のプール、128バイトのバッファ用のプールなどがあります。プールの合計メモリには制約がありますが、バッファが大きくなるにつれてGCヒープを「チャーン」することは避けてください。
質問ありがとうございます。
ポールハリントン。