2011-10-17 11:27:40
zlibsuballoc - lepsze zarządzanie pamięcią dla biblioteki zlib

Drobnica przygotowana na potrzeby Hellcore Mailera i paru innych moich projektów.

Problem jest błahy - zlib, rozpoczynając (de)kompresję, alokuje pewną ilość pamięci i dealokuje ją, gdy skończy. Pomnożyć to razy kilka tysięcy i wydajnościowa katastrofa gotowa. zlibsuballoc ma za zadanie tej katastrofie jeśli nie zapobiec, to przynajmniej zredukować ją do akceptowalnego poziomu, bez wpływania na cały program.

Nie będę wnikał w szczegóły, są one opisane w manualu, w każdym razie, każda kompresja obarczona jest narzutem pamięciowym w wysokości ok. 256 (+ileś)kB, zaś dekompresja - ok. 44 (+ew. ileś) kB. Każdy osobny cykl kompresji/dekompresji powoduje alokowanie i zwolnienie tej pamięci i jeśli nie jest używany sensowny subalokator, to zaczynają się problemy z błędami stron (ang. page faults). Jeśli występują za często, wydajność spada i program więcej czasu marnuje na zarządzanie pamięcią przez kernel. W skrajnym przypadku, może zajmować to tyle samo co właściwa kompresja: zaalokowanie 262144 dwukilobajtowych bloków pamięci i kompresowanie ich z osobna na Atomie330 i XP SP3 trwa 3 minuty i 29 sekund, zaś system nalicza w sumie 19 MILIONÓW błędów stron. Po zastosowaniu zlibsuballoc, czas spada do minuty i 13 sekund, liczba błędów stron spada do (w sumie) 134 TYSIĘCY. W realnych warunkach jest trochę inaczej: Hellcore Mailer, kompresując i dekompresując hurtem (ma do tego odpowiedni kod) 234675 maili (niecałe 400MB), produkował 10 218 410 błędów stron, sama kompresja trwała ok. 150 sekund, z czego 51 sekund spędził w kernelu. Po zaaplikowaniu zlibsuballoc, czas spadł do 74 sekund, w samym jądrze przesiedział niecałe dwie sekundy, wygenerował w sumie 60365 błędów stron. Różnica jest, i to poważna. Kompresor SMP też zyskał, ilość błędów stron spadła z 73801 do 55246 - tu jednak jakichś wydajnościowych rewolucji nie ma, z drugiej strony, on kompresował ok. 140 plików. Co nie zmienia że zysk jest.

Tyle wstępem zachęty. Technicznie wygląda to tak, że twórcy biblioteki zlib przewidzieli możliwość wykorzystania specjalizowanego menedżera pamięci (zlibsuballoc jest takowym), w strukturze z_stream_s znajduje się pole opaque (przez Borland/Inprise lolzowo przetłumaczone na AppData), i jego wartość jest przekazywana do funkcji alokujących i dealokujących pamięć. W tej samej strukturze znajdują się pola zalloc i zfree i wskazują na rzeczone funkcje menedżera pamięci. Jeśli są tam NULLe (NILe), wywoływane są standardowe funkcje. Zatem plan jest prosty - alokuję pamięć z góry, przed rozpoczęciem hurtowej (de)kompresji, potem używam wyłącznie tej pamięci, nie wykonując realnej alokacji/dealokacji, na koniec zwalniam ją.

Ta implementacja wymaga ustawienia wskaźników na własne funkcje zalloc i zfree tak, by nie kolidować z istniejącymi - tym, jak i wstępnym zaalokowaniem bloku pamieci, zajmuje się procedura SetZlibSuballocator, która jako argument przyjmuje strukturę TZStreamRec (przetłumaczona na Object Pascal struktura z_stream_s). Po zakończonym pojedyńczym cyklu kompresji/dekompresji należy wywołać procedurę FreeZlibSuballocator która doda używany blok pamięci do tablicy zwolnionych bloków, co pozwoli go wykorzystać przy następnym cyklu (de)kompresji. - SetZlibSuballocator weźmie ostatni blok z tej tablicy albo zaalokuje nowy, zatem nie ma problemu z kompresowaniem równoległym. UWAGA: FreeZlibSuballocator jedynie oznacza blok jako wolny. Zwolnieniem wszystkich bloków zajmuje się procedura ReleaseZlibSuballocatorMemory. Wywołanie jej odda do systemu całą dotychczas użytą pamięć, zatem używać jej należy tylko na sam koniec.
No i jeszcze jedna uwaga, mianowicie podobnie jak deflateEnd czy inflateEnd, FreeZlibSuballocator zawsze musi być wywołane, w przeciwnymrazie dojdzie do wycieków pamięci. Wywołanie można umieścić np. w sekcji "finally" bloku "try"..."finally".

Implememtacja alokatora jest makabrycznie prosta. SetZlibSuballocator alokuje (albo recykluje pierwszy wolny) 320kb blok pamięci (regulowane stałą), zaś kolejne wywołania zcalloc_pbuf (właściwej funkcji przydzielającej pamięć) powodują przesuwanie wskaźnika następnego bloku w dół, począwszy od właściwego początku zaalokowanego obszaru pamięci. Wskaźnik jest wyrównywany do 16 bajtów w dół, przez co nie ma problemu z wyrównaniem i jednocześnie marnowaniem pamięci (użycie VirtualAlloc zapewniłoby zmarnowamie do 4095 bajtów). Dealokatora nie ma. Założyłem, że alokacje wykonuje się raz na początku, a dealokacje na końcu - wobec czego, dealokator nic nie robi. Ponieważ wskaźnik posuwa się w dół, w końcu przekroczy 320kB - dlatego też tak ważne jest wołanie FreeZlibSuballocator po cyklu (de)kompresji. Bez tego ten blok nigdy nie będzie wykorzystany ponownie przez procedurę SetZlibSuballocator, która - widząc już zaalokowany, wolny blok - przesunie wskaźnik spowrotem na początek. Szybkie? Szybkie.

Kod jest dostępny na BranchWare. Nie myślałem o żadnej sensownej licencji, więc na razie public domain - potem ewentualnie pomyślę o czymś innym. Kod obecnie na licencji BSD. Moduł zlibsuballoc.pas zawiera alokator, dostosowane wersje procedur CompressBufBP, DecompressBufBP i DecompressToUserBufBP, które wykorzystują ten alokator. Klasy takie jak TCompressionStream niestety trzeba zmodyfikować ręcznie - z drugiej strony, wydaje mi się, że nie jest to trudne, a poza tym, warto "pogadać" ze zlibem bezpośrednio, nie przez ograniczające otoczki, ponieważ w ten sposób można zdziałać dużo więcej. Może kiedyś o tym też napiszę.


Może Cię zainteresować...

Komentowanie wyłączone dla tego wpisu.
Powered by:
Hellcore Mailer - polski program pocztowyOpera Web BrowserFreeBSD - The Power to Serve!Slackware
RSSy:
Sidekick:
Projekty:
O autorze:
Zobacz:
Kategorie:
Archiwum:
Szukaj: