2012-05-21 13 views
15

Używam niestandardowej implementacji sterty w jednym z moich projektów. Składa się z dwóch głównych części:Sterty zoptymalizowane do (bez ograniczenia) użycia jednowątkowego

  1. Naprawiono stertę wielkości bloku. To znaczy. sterty, która przydziela bloki tylko określonego rozmiaru. Przydziela większe bloki pamięci (strony pamięci wirtualnej lub z innego sterty), a następnie dzieli je na jednostki przydzielania energii atomowej.

    Szybko wykonuje alokację/uwolnienie (w O (1)) i nie ma narzutów na pamięć, nie biorąc pod uwagę rzeczy narzuconych przez stertę zewnętrzną.

  2. Zasobnik ogólnego zastosowania ogólnego przeznaczenia. Składa się z wiader z wyżej wymienionych (stałych) hałd. WRT żądany rozmiar alokacji wybiera odpowiedni zasobnik i wykonuje alokację za jego pośrednictwem.

    Ponieważ cała aplikacja jest (mocno) wielowątkowa - globalna sterty blokuje odpowiednią łyżkę podczas jej pracy.

    Uwaga: w przeciwieństwie do tradycyjnych hałdów, ta sterta wymaga wielkości alokacji nie tylko dla przydziału, ale także dla uwolnienia. Pozwala to na identyfikację odpowiedniego zasobnika bez przeszukiwania lub dodatkowego obciążenia pamięci (np. Zapisanie rozmiaru bloku przed przydzielonym blokiem). Choć nieco mniej wygodne, w moim przypadku jest OK. Ponadto, ponieważ "konfiguracja kubełkowa" jest znana w czasie kompilacji (zaimplementowana za pośrednictwem szablonu voodoo C++) - odpowiednia łyżka jest określana w czasie kompilacji.

Do tej pory wszystko wygląda (i działa) dobrze.

Ostatnio pracowałem nad algorytmem, który wykonuje operacje sterty ciężko i naturalnie wpływa znacząco na wydajność sterty. Profilowanie ujawniło, że jego działanie jest znacząco ograniczone przez blokowanie blokowania. Oznacza to, że sama sterta działa bardzo szybko (typowa alokacja wymaga tylko kilku instrukcji do dereferencji pamięci), ale ponieważ cała aplikacja jest wielowątkowa - odpowiednia łyżka jest chroniona przez sekcję krytyczną, która opiera się na instrukcjach połączonych, które są znacznie cięższe.

Naprawiłem to tymczasem, nadając temu algorytmowi własną wydzieloną stertę, która nie jest chroniona przez krytyczną sekcję. Wymaga to jednak kilku problemów/ograniczeń na poziomie kodu. Takich jak potrzeba przekazywania informacji kontekstowych głęboko w stosie, gdziekolwiek sterty mogą być konieczne. Można również użyć TLS, aby tego uniknąć, ale może to powodować problemy z ponownym wejściem w moim konkretnym przypadku.

Zastanawiam się: czy istnieje znana technika optymalizacji stosu do (ale nie ogranicza się do) użycia jednowątkowego?

EDIT:

Specjalne podziękowania dla @Voo dla sugeruje sprawdzanie tcmalloc przez Google.

Wydaje się działać podobnie do tego, co zrobiłem mniej lub bardziej (przynajmniej dla małych obiektów). Ale dodatkowo rozwiązują dokładnie ten sam problem, utrzymując w pamięci wątku buforowanie.

Ja też myślałem w tym kierunku, ale myślałem o zachowaniu w wątku hałdy. Następnie zwolnienie bloku pamięci przydzielonego ze sterty należącego do innego wątku jest nieco trudne: należy go wstawić w rodzaj zablokowanej kolejki, a ten inny wątek powinien zostać powiadomiony i asynchronicznie uwolnić oczekujące alokacje. Asynchroniczna dealokacja może powodować problemy: jeśli wątek jest zajęty z jakiegoś powodu (na przykład wykonuje agresywne obliczenia) - w rzeczywistości nie ma deallokacji pamięci. Dodatkowo w scenariuszu wielowątkowym koszt dealokacji jest znacznie wyższy.

OTOH pomysł z buforowaniem wydaje się znacznie prostszy i bardziej wydajny. Spróbuję to rozpracować.

Wielkie dzięki.

P.S .:

Rzeczywiście tcmalloc Google jest wielki. Uważam, że jest zaimplementowany w sposób podobny do tego, co zrobiłem (przynajmniej część o stałej wielkości).

Ale to pedantyczna sprawa, w której moja kupa jest lepsza. Według dokumentów, tcmalloc nakłada narzut około 1% (asymptotycznie), podczas gdy mój narzut wynosi 0,0061%. Dokładnie 4/64K.

:)

+0

To pamięta Moje strony do testów zrobiłem rok temu. Powszechnie stosowany "słaby" standardowy mechanizm zajmuje ponad 100 razy dobrą, "niestandardową" implementację. –

+1

Chciałbym zobaczyć, co zrobiłeś, jeśli jest szybszy niż standardowy alokator pamięci. Ponieważ większość standardowych implementacji już robi to, co twierdzi twój (i wiele więcej). Uważam, że roszczenie O (1) jest ciekawe, szczególnie gdy nie dochodzisz roszczenia (jestem pewien, że zrobisz niezłą monetę, gdy twój patent na to przechodzi). –

+1

Cały pomysł na wiadro to w zasadzie tcmalloc google'a (chociaż ponieważ jest to ogólny alokator, musi on dynamicznie decydować, którego kubełka użyć). tcmalloc używa lokalnej pamięci wątku, aby uniknąć dokładnie problemu i rzadko przydziela się z ogólnej sterty, a tym samym unika blokad. – Voo

Odpowiedz

10

Jedna myśl jest utrzymanie przydzielania pamięci per-wątku. Wstępnie przypisz dość grube bloki pamięci do każdego przydziału z globalnej puli pamięci. Zaprojektuj swój algorytm, aby przypisać masywne klocki z sąsiednich adresów pamięci (więcej o tym później).

Gdy program przydzielający dany wątek ma mało pamięci, żąda większej ilości pamięci z globalnej puli pamięci. Ta operacja wymaga blokady, ale powinna występować znacznie rzadziej niż w twoim przypadku. Kiedy alokator dla danego wątku zwalnia ostatni bajt, zwróć całą pamięć tego przydziału do globalnej puli pamięci (zakładając, że wątek jest zakończony).

Takie podejście będzie miało tendencję do wyczerpywania pamięci wcześniej niż obecne podejście (pamięć może być zarezerwowana dla jednego wątku, który nigdy jej nie potrzebuje). Zakres, w jakim jest to problem, zależy od profilu tworzenia/trwałości/zniszczenia wątku twoich aplikacji. Można to złagodzić kosztem dodatkowej złożoności, np. przez wprowadzenie sygnału, że alokator pamięci dla danego wątku jest poza pamięcią, a pula globalna jest exhaused, na którą inne alokatory pamięci mogą odpowiedzieć, zwalniając część pamięci.

Zaletą tego schematu jest tendencja do eliminowania false sharing, ponieważ pamięć dla danego wątku będzie zazwyczaj przydzielana w ciągłych przestrzeniach adresowych.

Na marginesie, jeśli jeszcze go nie czytałeś, proponuję artykuł IBM's Inside Memory Management dla każdego, kto wdraża własne zarządzanie pamięcią.

UPDATE

Jeśli celem jest, aby mieć bardzo szybki przydział pamięci zoptymalizowany dla środowisku wielowątkowym (w przeciwieństwie do uczenia się, jak to zrobić samemu), rzucić okiem na przemian podzielników pamięci. Jeśli celem jest nauka, być może sprawdź ich kod źródłowy.

+0

Oprócz Hoarde istnieje również [tcmalloc] (http://goog-perftools.sourceforge.net/doc/tcmalloc.html), który jest w zasadzie proponowanym planem operacyjnym z oczywistym (wątku lokalnego sterty) i mniej oczywistymi usprawnieniami. Kiedy testowałem to było szybsze niż Hoarde, ale zakładam, że zależy to od przypadków użycia. – Voo

+0

@Voo: dokładniej, nie jest to wątek-lokalny * stos *, ale lokalny * wątek *, który jest zupełnie innym zwierzęciem. Przeczytaj moje zaktualizowane pytanie. P.S. Chciałbym również, abyś porównał moją kupę, żeby zobaczyć, jak się ona porównuje. – valdo

+0

Dzięki za sugestie. Podobało mi się, że staram się alokować przyległe bloki pamięci na wątek, aby zmniejszyć prawdopodobieństwo fałszywego udostępniania. Jednak IMHO prawdziwym "słodkim punktem" jest ** buforowanie ** dla wątków, a nie dla wątków * przydzielających *. Jest niezwykle prosty i prawdopodobnie zapewnia najlepszą możliwą wydajność w przypadku użycia jednowątkowego, bez znacznych kar do wydajności wielowątkowej. Tak, szanse na fałszywe udostępnianie są wyższe, ale w typowych scenariuszach będzie to niewielka IMHO. – valdo

0

To może być dobry pomysł, aby przeczytać Jeff Bonwicks klasyczne papiery na podzielnika płyty i vMem. Oryginalny alokator płyt brzmi trochę, co robisz. Chociaż nie jest to bardzo przyjazny dla wielu użytkowników, może dać ci kilka pomysłów.

The Slab Allocator: An Object-Caching Kernel Memory Allocator

Potem rozszerzył koncepcję z vMem, co z pewnością daje pewne pomysły, ponieważ miał bardzo ładne zachowanie w środowisku wielu CPU.

Magazines and Vmem: Extending the Slab Allocator to Many CPUs and Arbitrary Resources