2015-06-03 15 views
26

Model jest typem używanym w kompilatorze Roslyn C# do wielokrotnego użytku często używanych obiektów, które zwykle bardzo często pobierają nowe wiadomości i zbierają śmieci. Zmniejsza to ilość i rozmiar operacji zbierania śmieci, które muszą się wydarzyć.Dlaczego jest tak wiele implementacji Object Pooling w Roslyn?

Kompilator Roslyn wydaje się mieć kilka oddzielnych pul obiektów, a każda pula ma inny rozmiar. Chcę wiedzieć, dlaczego jest tak wiele implementacji, jaka jest preferowana implementacja i dlaczego wybrano pulę o wielkości 20, 100 lub 128.

1 - SharedPools - Przechowuje pulę 20 obiektów lub 100, jeśli BigDefault jest używany. Ta jest również dziwna, ponieważ tworzy nową instancję PooledObject, co nie ma sensu, gdy próbujemy połączyć obiekty, a nie tworzyć i niszczyć nowe.

// 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 i StringBuilderPool - Nie ściśle odrębne implementacje ale obwolut wokół realizacji SharedPools przedstawione powyżej specjalnie dla listy i StringBuilder jest. W ten sposób ponownie wykorzystuje pulę obiektów przechowywanych w 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 i PooledHashSet - Są używać ObjectPool bezpośrednio i mają całkowicie odrębną pulę obiektów. Przechowuje pulę 128 obiektów.

// 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(); 
} 

Aktualizacja

Są nowe implementacje obiekt grupowania w .NET Core. Zobacz moją odpowiedź na pytanie C# Object Pooling Pattern implementation.

+3

Biorąc pod uwagę, że Microsoft zawsze opierał się koncepcji pul obiektów w .NET, ponieważ zawsze twierdził, że GC obiektów gen0 jest bardzo szybki, jest to interesujący zwrot :-) – xanatos

+0

Po przeczytaniu, myślę, że kompilator Roslyn jest specjalnym razem z StackOverflow, co jest kolejnym przykładem, jaki znam, który to robi. Obiekt gromadzący chroni przed zbiorami gen2, które mogą powodować przerwy w aplikacji drugiego lub więcej, chociaż nie jestem pewien dokładnego czasu. Wadą tego podejścia jest to, że zużywasz więcej pamięci, ponieważ nigdy nie usuwasz połączonych obiektów. –

+1

Kompilator nie jest aplikacją czasu rzeczywistego, w której nie chcesz pauzować ... I nie jest to tak, że setka użytkowników łączy się jednocześnie. Chcieli go zoptymalizować, ponieważ nie chcieli, aby był wolniejszy od starszego kompilatora i korzystali z Object Pool ... Ale to nie znaczy, że muszę polubić to, co zrobili. – xanatos

Odpowiedz

35

Jestem liderem zespołu v Roslyn. Wszystkie pule obiektów zostały zaprojektowane w celu zmniejszenia współczynnika alokacji, a tym samym częstotliwości zbierania śmieci. Dzieje się to kosztem dodawania długotrwałych (gen 2) obiektów. Pomaga to nieznacznie zwiększyć przepustowość kompilatora, ale głównym efektem jest szybkość reakcji Visual Studio podczas używania VB lub C# IntelliSense.

dlaczego istnieje tak wiele implementacji”

Nie ma szybkie odpowiedzi, ale myślę, że z trzech powodów:.

  1. Każde wdrożenie serwuje nieco inny cel i są dostrojone
  2. "Warstwa" - wszystkie pule są wewnętrznymi i wewnętrznymi szczegółami z warstwy kompilatora, które nie mogą być przywoływane z warstwy Workspace lub odwrotnie. połączonych plików, ale staramy się ograniczyć to do minimum.
  3. Nie dołożono wielkich starań, aby ujednolicić implementacje, które widzicie dzisiaj.

co preferowana realizacja jest

ObjectPool<T> jest preferowana realizacja i co większość kodu używa. Zauważ, że ObjectPool<T> jest używane przez ArrayBuilder<T>.GetInstance() i jest to prawdopodobnie największy użytkownik połączonych obiektów w Roslyn.Ponieważ ObjectPool<T> jest tak intensywnie wykorzystywany, jest to jeden z przypadków, w których skopiowaliśmy kod po warstwach za pośrednictwem połączonych plików. ObjectPool<T> jest dostrojony pod kątem maksymalnej przepustowości.

W warstwie roboczej zobaczysz, że SharedPool<T> próbuje udostępnić połączone instancje dla rozłącznych komponentów, aby zmniejszyć ogólne wykorzystanie pamięci. Staraliśmy się uniknąć sytuacji, w której każdy komponent tworzyłby własną pulę przeznaczoną do określonego celu, a zamiast tego współużytkować w oparciu o typ elementu. Dobrym tego przykładem jest StringBuilderPool.

dlaczego wybrał wielkość puli 20, 100 lub 128.

Zazwyczaj jest to wynikiem profilowania i instrumentacji w typowych obciążeń. Zazwyczaj musimy zachować równowagę między stopą alokacji ("pomyłki" w puli) i łączną liczbą aktywnych bajtów w puli. Te dwa czynniki w grze są:

  1. Maksymalny stopień równoległości (współbieżnych wątków dostęp do basenu)
  2. Wzór Dostęp tym pokrywały alokacji i zagnieżdżonych alokacji.

W wielkim układzie rzeczy pamięć przechowywana przez obiekty w puli jest bardzo mała w porównaniu do całkowitej pamięci na żywo (wielkość sterty Gen 2) do kompilacji, ale staramy się również nie zwróć gigantyczne obiekty (zazwyczaj duże kolekcje) z powrotem do puli - po prostu upuścimy je na podłodze, dzwoniąc pod numer ForgetTrackedObject

W przyszłości myślę, że jednym z obszarów, które możemy poprawić, jest posiadanie puli tablic bajtowych (bufory) o ograniczonych długościach. Pomoże to w szczególności w implementacji MemoryStream w fazie emisji (PEWriter) kompilatora. Te moduły MemoryStream wymagają przylegających tablic bajtów do szybkiego zapisu, ale mają one dynamiczny rozmiar. Oznacza to, że czasami trzeba zmienić rozmiar - zwykle podwajając rozmiar za każdym razem. Każda zmiana rozmiaru jest nową alokacją, ale byłoby miło móc pobrać zmieniony bufor z dedykowanej puli i przywrócić mniejszy bufor z powrotem do innej puli. Na przykład, masz pulę na bufory 64-bajtowe, kolejną na bufory 128-bajtowe i tak dalej. Całkowita pamięć puli będzie ograniczona, ale unikniesz "zawracania" sterty GC w miarę wzrostu buforów.

Jeszcze raz dziękuję za pytanie.

Paul Harrington.

+3

Nie, dziękuję Pawłowi za odpowiedź! To jest to, co uwielbiam w StackOverflow, zadaj pytanie dotyczące jakiegoś oprogramowania, a programista pojawia się i odpowiada ci. Szukam puli obiektów dla mojego projektu [ASP.NET MVC Boilerplate] (https://visualstudiogallery.msdn.microsoft.com/6cf50a48-fc1e-4eaf-9e82-0b2a6705ca7d). –

+0

Dlaczego nie możemy po prostu pozbyć się tego reliktu z lat 90-tych ??? Teraz to open source powinniśmy robić to, co Delphi robi na urządzeniach mobilnych, a cel-c automatycznie uwalnia dany obiekt automatycznie. w razie potrzeby umieszcza wywołanie Free(). – Joe

Powiązane problemy