2014-12-09 12 views
5

Posiadamy usługę internetową wykorzystującą WebApi 2, .NET 4.5 na serwerze 2012. Obserwowaliśmy sporadyczne opóźnienia wzrastające o 10-30ms bez uzasadnionego powodu. Udało nam się wyśledzić problematyczny fragment kodu do LOH i GC.Rozległe wykorzystanie LOH powoduje znaczny problem z wydajnością.

Istnieje pewien tekst, który konwertujemy na jego reprezentację bajtów UTF8 (w rzeczywistości biblioteka do serializacji, której używamy). Dopóki tekst jest krótszy niż 85000 bajtów, opóźnienie jest stabilne i krótkie: średnio około 0,2 ms i co najmniej 99%. Po przekroczeniu granicy 85000, średnie opóźnienie wzrasta do ~ 1 ms, podczas gdy 99% przeskakuje do 16-20ms. Profiler pokazuje, że większość czasu spędza się w GC. Aby mieć pewność, jeśli wstawię GC.Collect między iteracjami, zmierzone opóźnienie sięga 0,2 ms.

Mam dwa pytania:

  1. Skąd opóźnienie pochodzi? O ile rozumiem, LOH nie jest zagęszczony. SOH jest kompaktowany, ale nie wykazuje opóźnienia.
  2. Czy istnieje praktyczny sposób obejścia tego? Uwaga: nie mogę kontrolować rozmiaru danych i zmniejszać ich.

-

public void PerfTestMeasureGetBytes() 
{ 
    var text = File.ReadAllText(@"C:\Temp\ContactsModelsInferences.txt"); 
    var smallText = text.Substring(0, 85000 + 100); 
    int count = 1000; 
    List<double> latencies = new List<double>(count); 
    for (int i = 0; i < count; i++) 
    { 
     Stopwatch sw = new Stopwatch(); 
     sw.Start(); 
     var bytes = Encoding.UTF8.GetBytes(smallText); 
     sw.Stop(); 
     latencies.Add(sw.Elapsed.TotalMilliseconds); 

     //GC.Collect(2, GCCollectionMode.Default, true); 
    } 

    latencies.Sort(); 
    Console.WriteLine("Average: {0}", latencies.Average()); 
    Console.WriteLine("99%: {0}", latencies[(int)(latencies.Count * 0.99)]); 
} 
+0

Czy ta pomoc http://stackoverflow.com/questions/686950/large-object-heap-fragmentation – Sid

+0

nie jest tak naprawdę, to mówi o rozdrobnieniu i moim problemem jest opóźnienie. –

Odpowiedz

5

Problemy wydajności zazwyczaj pochodzą z dwóch obszarów: przydział i rozdrobnienie.

Alokacja

Środowisko wykonawcze gwarantuje czysty pamięć tak spędza cykli czyszczenia. Kiedy przydzielasz duży obiekt, to jest dużo pamięci i zaczynasz dodawać milisekundy do pojedynczej alokacji (kiedy pozwalasz być szczerym, prosta alokacja w .NET jest w rzeczywistości bardzo szybka, więc zwykle nigdy się tym nie przejmujemy).

Fragmentacja występuje, gdy obiekty LOH są przydzielane, a następnie odzyskiwane. Do niedawna GC nie mógł zreorganizować pamięci, aby usunąć te stare luki "obiektu", a zatem mógł dopasować tylko kolejny obiekt w tej luce, gdyby był tego samego rozmiaru lub mniejszy. Ostatnio GC ma możliwość kompaktowania LOH, co usuwa ten problem, ale kosztuje czas podczas zagęszczania.

MyPrzypuszczam, że w obu przypadkach masz problemy z uruchomieniem GC, ale zależy to od tego, jak często Twój kod próbuje przydzielić elementy w LOH. Jeśli wykonujesz wiele alokacji, spróbuj użyć trasy łączenia obiektów. Jeśli nie możesz efektywnie kontrolować puli (nierówne okresy istnienia obiektu lub odmienne wzorce użycia), spróbuj porcjować dane, przeciwko którym pracujesz, aby uniknąć ich całkowicie.


Opcji

Napotkałem dwa podejścia do LOH:

  • uniknąć.
  • Użyj go, ale zdaj sobie sprawę, że go używasz i zarządzaj nim jawnie.

Unikaj to

Wiąże wyrwy swój duży obiekt (zwykle tablicę jakiegoś) do, dobrze, kawałki, które każdy spadek poniżej bariery LOH. Robimy to podczas szeregowania strumieni dużych obiektów. Działa dobrze, ale implementacja byłaby specyficzna dla twojego środowiska, więc nie chcę podać kodowanego przykładu.

Użyj go

Prostym sposobem rozwiązania zarówno alokacji i fragmentacji jest długowieczne obiekty. Jawnie utwórz pustą tablicę (lub tablice) o dużym rozmiarze, aby pomieścić duży obiekt i nie pozbądź się go (lub ich). Zostaw to i korzystaj z niego jak z puli obiektów. Płacisz za tę alokację, ale możesz to zrobić przy pierwszym użyciu lub w czasie bezczynności aplikacji, ale płacisz mniej za ponowną alokację (ponieważ nie zmieniasz alokacji) i zmniejszasz problemy z fragmentacją, ponieważ nie musisz ciągle pytać przydzielaj rzeczy i nie odzyskujesz przedmiotów (co powoduje luki w pierwszej kolejności).

To powiedziawszy, dom w połowie drogi może być w porządku. Zarezerwuj część pamięci z góry dla puli obiektów. Zrobione wcześnie, alokacje te powinny być ciągłe w pamięci, aby uniknąć luk i pozostawić koniec dostępnej pamięci dla niekontrolowanych elementów. Należy jednak pamiętać, że ma to oczywiście wpływ na zestaw roboczy aplikacji - pula obiektów zajmuje miejsce, niezależnie od tego, czy jest używana, czy nie.


Resources

LOH pokryta jest dużo w internecie, ale należy zwrócić uwagę na daty zasobu. W najnowszych wersjach .NET LOH otrzymał trochę miłości i poprawił się. Powiedział, że jeśli masz starszą wersję, uważam, że zasoby w sieci są dość dokładne, ponieważ LOH nigdy nie otrzymywał poważnych aktualizacji od dłuższego czasu między początkiem a .NET 4.5 (ish).

Na przykład, nie jest to artykuł z 2008 http://msdn.microsoft.com/en-us/magazine/cc534993.aspx

oraz podsumowanie ulepszeń w .NET 4.5: http://blogs.msdn.com/b/dotnet/archive/2011/10/04/large-object-heap-improvements-in-net-4-5.aspx

+0

@downvoter Chciałbym poznać powód upadku. Moje rozumienie LOH nie jest kompletne, więc chciałbym wypełnić wszystkie luki. Proszę, pomóż mi i pomóż mi poprawić odpowiedź. Od czasu przegłosowania zmieniłem nieco moją odpowiedź, więc mam nadzieję, że poprawiłem ten problem, ale nie wiedziałbym. –

+0

Dzięki Adamowi, problem z obydwoma sugerowanymi rozwiązaniami polega na tym, że mamy do czynienia z obiektami stworzonymi przez osoby trzecie. Jednym z pól jest ciąg, który akurat znajdował się powyżej progu. Tak szybko, jak to się dzieje, cały system zwalnia. Ciąg przechodzi od postaci szeregowej do łańcucha, jest kopiowany kilka razy z powodu własnej logiki biznesowej. Ponieważ nie kontroluję obiektu, nie mogę zastąpić łańcucha czymś innym, co byłoby bardziej przyjazne dla LOH. –

+0

Dam mu jeszcze trochę czasu i zaakceptuję twoją odpowiedź, jeśli nie znajdę/nie otrzymam lepszej. –

3

Oprócz Poniżej upewnić że używasz server garbage collector . Nie ma to wpływu na sposób użycia LOH, ale moje doświadczenie jest takie, że znacznie zmniejsza ilość czasu spędzanego w GC.

Najlepszym sposobem na uniknięcie problemów z dużymi obiektami jest stworzenie trwałego bufora i ponowne użycie go. Zamiast przypisywać nową tablicę bajtów przy każdym wywołaniu do Encoding.GetBytes, należy przekazać tablicę bajtów do metody.

W tym przypadku należy użyć GetBytes overload, który pobiera tablicę bajtów. Przydziel tablicę, która jest wystarczająco duża, aby pomieścić bajty dla twojego najdłuższego spodziewanego ciągu i zachować ją w pobliżu. Na przykład:

// allocate buffer at class scope 
private byte[] _theBuffer = new byte[1024*1024]; 

public void PerfTestMeasureGetBytes() 
{ 
    // ... 
    for (...) 
    { 
     var sw = Stopwatch.StartNew(); 
     var numberOfBytes = Encoding.UTF8.GetBytes(smallText, 0, smallText.Length, _theBuffer, 0); 
     sw.Stop(); 
     // ... 
    } 

Jedyny problem polega na tym, że musisz upewnić się, że twój bufor jest wystarczająco duży, aby pomieścić największy ciąg. To, co zrobiłem w przeszłości, to przydzielić bufor do największego oczekiwanego rozmiaru, ale potem sprawdź, czy jest wystarczająco duży, ilekroć go używam. Jeśli nie jest wystarczająco duży, należy go ponownie przydzielić. Sposób, w jaki to robisz, zależy od tego, jak rygorystycznie chcesz być. Podczas pracy z tekstem głównie zachodnioeuropejskim po prostu podwoiłbym długość ciągu.Na przykład:

string textToConvert = ... 
if (_theBuffer.Length < 2*textToConvert.Length) 
{ 
    // reallocate the buffer 
    _theBuffer = new byte[2*textToConvert.Length]; 
} 

Innym sposobem, aby to zrobić, to po prostu spróbuj GetString i realokacji na niepowodzenie. Następnie spróbuj ponownie. Na przykład:

while (!good) 
{ 
    try 
    { 
     numberOfBytes = Encoding.UTF8.GetString(theString, ....); 
     good = true; 
    } 
    catch (ArgumentException) 
    { 
     // buffer isn't big enough. Find out how much I really need 
     var bytesNeeded = Encoding.UTF8.GetByteCount(theString); 
     // and reallocate the buffer 
     _theBuffer = new byte[bytesNeeded]; 
    } 
} 

Jeśli się bufora początkowy rozmiar wystarczająco duże, aby pomieścić największy łańcuch można oczekiwać, to prawdopodobnie nie będzie się tego wyjątku bardzo często. Oznacza to, że liczba ponownych przydziałów bufora będzie bardzo mała. Możesz oczywiście dodać dopełnienie do bytesNeeded, aby przydzielić więcej, na wypadek, gdyby pojawiły się inne wartości odstające.

+1

Warto zauważyć, że przy korzystaniu z bufora trzeba zignorować część bufora, która nie jest potrzebna dla obiektu, więc twój kod do iteracji lub cokolwiek musi zwracać uwagę na rozmiar rzeczywistych danych wewnątrz i nie może polegać na 'Array.Length'. –

+0

@AdamHouldsworth: True. Ale właśnie do tego służy wartość zwracana z 'GetBytes'. . . –

+0

Rzeczywiście, nie krytykuję rozwiązania. Robiłem to już w przeszłości, ale łatwo jest zapomnieć, że kiedy pracujesz pod tym rozwiązaniem, twoja mała tablica może zostać umieszczona w znacznie większym. Ta technika może być również używana do uniknięcia przydzielania wielu małych tablic na LOH lub poza nim. –

Powiązane problemy