Zarządzanie pamięcią do szybkiego przesyłania wiadomości między wątkami w C ++

9

Załóżmy, że istnieją dwa wątki, które komunikują się poprzez asynchroniczne wysyłanie do siebie komunikatów danych. Każdy wątek ma jakąś kolejkę komunikatów.

Moje pytanie jest bardzo niskie: czego można się spodziewać jako najbardziej efektywnego sposobu zarządzania pamięcią? Mogę wymyślić kilka rozwiązań:

  1. Nadawca tworzy obiekt przez new. Połączenia z odbiorcą delete.
  2. Pula pamięci (aby przenieść pamięć z powrotem do nadawcy)
  3. Wywóz śmieci (np. Boehm GC)
  4. (jeśli obiekty są wystarczająco małe) kopiuj według wartości, aby całkowicie uniknąć alokacji sterty

1) jest najbardziej oczywistym rozwiązaniem, więc użyję go jako prototypu. Są szanse, że jest już wystarczająco dobry. Ale niezależnie od mojego konkretnego problemu, zastanawiam się, która technika jest najbardziej obiecująca, jeśli optymalizujesz wydajność.

Spodziewałbym się, że pula teoretycznie jest najlepsza, zwłaszcza że możesz wykorzystać dodatkową wiedzę na temat przepływu informacji między wątkami. Obawiam się jednak, że najtrudniej jest też dobrze się postarać. Dużo tuningu ... :-(

Odśmiecanie powinno być dość łatwe do późniejszego dodania (po rozwiązaniu 1) i spodziewałbym się, że będzie działał bardzo dobrze. Sądzę więc, że jest to najbardziej praktyczne rozwiązanie, jeśli 1) okaże się zbyt nieefektywne.

Jeśli obiekty są małe i proste, kopiowanie według wartości może być najszybsze. Obawiam się jednak, że wymusza to niepotrzebne ograniczenia w implementacji obsługiwanych komunikatów, więc chcę tego uniknąć.

Philipp Claßen
źródło

Odpowiedzi:

9

Jeśli obiekty są małe i proste, kopiowanie według wartości może być najszybsze. Obawiam się jednak, że wymusza to niepotrzebne ograniczenia w implementacji obsługiwanych komunikatów, więc chcę tego uniknąć.

Jeśli możesz przewidzieć górną granicę char buf[256], np. Praktyczną alternatywę, jeśli nie możesz, która wywołuje przydziały sterty tylko w rzadkich przypadkach:

struct Message
{
    // Stores the message data.
    char buf[256];

    // Points to 'buf' if it fits, heap otherwise.
    char* data;
};

źródło
3

Będzie to zależeć od sposobu implementacji kolejek.

Jeśli korzystasz z tablicy (okrągły styl robin), musisz ustawić górną granicę rozmiaru dla rozwiązania 4. Jeśli korzystasz z połączonej kolejki, potrzebujesz przydzielonych obiektów.

Następnie można łatwo łączyć zasoby, zastępując nowy i usuwając za pomocą AllocMessage<T>i freeMessage<T>. Moją sugestią byłoby ograniczenie liczby potencjalnych rozmiarów Ti zaokrąglanie w górę przy przydzielaniu betonu messages.

Proste czyszczenie pamięci może działać, ale może to powodować długie przerwy, gdy trzeba zebrać dużą część, i będzie (moim zdaniem) działać nieco gorzej niż nowy / usuń.

maniak zapadkowy
źródło
3

Jeśli jest w C ++, po prostu użyj jednego ze inteligentnych wskaźników - Unique_ptr działałby dla ciebie dobrze, ponieważ nie usunie obiektu leżącego u podstaw, dopóki nikt nie będzie miał do niego żadnego uchwytu. Przekazujesz obiekt ptr do odbiornika według wartości i nigdy nie musisz się martwić, który wątek powinien go usunąć (w przypadkach, gdy odbiornik nie odbiera obiektu).

Nadal będziesz musiał poradzić sobie z blokowaniem między wątkami, ale wydajność będzie dobra, ponieważ nie zostanie skopiowana żadna pamięć (tylko sam obiekt ptr, który jest niewielki).

Przydzielanie pamięci na stercie nie jest najszybszą rzeczą, więc pula jest wykorzystywana, aby uczynić to znacznie szybszym. Po prostu pobierasz następny blok ze stosu o wstępnie ustalonej wielkości w puli, więc po prostu użyj do tego istniejącej biblioteki .

gbjbaanb
źródło
2
Blokowanie jest zwykle o wiele większym problemem niż kopiowanie pamięci. Tylko mówię.
tdammers
Kiedy piszesz unique_ptr, myślę, że masz na myśli shared_ptr. Ale chociaż nie ma wątpliwości, że użycie inteligentnego wskaźnika jest dobre do zarządzania zasobami, nie zmienia to faktu, że używasz jakiejś formy alokacji pamięci i dezalokacji. Myślę, że to pytanie jest bardziej ogólne.
5gon12eder
3

Największym osiągnięciem przy przekazywaniu obiektu z jednego wątku do drugiego jest narzut związany z chwytaniem zamka. Jest to rzędu kilku mikrosekund, co oznacza znacznie więcej niż średni czas, jaki zajmuje para new/ delete(rzędu stu nanosekund). Rozsądne newimplementacje starają się unikać blokowania za niemal wszystkimi kosztami, aby uniknąć spadku wydajności.

To powiedziawszy, chcesz upewnić się, że nie musisz chwytać zamków podczas komunikowania obiektów z jednego wątku do drugiego. Znam dwie ogólne metody osiągnięcia tego celu. Oba działają tylko w jednym kierunku między jednym nadawcą a jednym odbiorcą:

  1. Użyj bufora dzwonka. Oba procesy kontrolują jeden wskaźnik do tego bufora, jeden jest wskaźnikiem odczytu, drugi jest wskaźnikiem zapisu.

    • Nadawca najpierw sprawdza, czy jest miejsce na dodanie elementu, porównując wskaźniki, a następnie dodaje element, a następnie zwiększa wskaźnik zapisu.

    • Odbiornik sprawdza, czy istnieje element do odczytu, porównując wskaźniki, a następnie czyta element, a następnie zwiększa wskaźnik odczytu.

    Wskaźniki muszą być atomowe, ponieważ są dzielone między wątkami. Jednak każdy wskaźnik jest modyfikowany tylko przez jeden wątek, drugi potrzebuje tylko dostępu do odczytu wskaźnika. Elementy w buforze mogą być samymi wskaźnikami, co pozwala łatwo dopasować rozmiar bufora pierścieniowego do rozmiaru, który nie spowoduje zablokowania nadawcy.

  2. Użyj połączonej listy, która zawsze zawiera co najmniej jeden element. Odbiorca ma wskaźnik do pierwszego elementu, nadawca ma wskaźnik do ostatniego elementu. Te wskaźniki nie są udostępniane.

    • Nadawca tworzy nowy węzeł dla listy połączonej, ustawiając jego nextwskaźnik na nullptr. Następnie aktualizuje nextwskaźnik ostatniego elementu, aby wskazywał na nowy element. Na koniec zapisuje nowy element we własnym wskaźniku.

    • Odbiornik obserwuje nextwskaźnik pierwszego elementu, aby sprawdzić, czy są dostępne nowe dane. Jeśli tak, usuwa stary pierwszy element, przesuwa swój własny wskaźnik, aby wskazywał na bieżący element i rozpoczyna przetwarzanie.

    W tym ustawieniu nextwskaźniki muszą być atomowe, a nadawca musi upewnić się, że nie odrzuci drugiego ostatniego elementu po ustawieniu nextwskaźnika. Zaletą jest oczywiście to, że nadawca nigdy nie musi blokować.

Oba podejścia są znacznie szybsze niż jakiekolwiek podejście oparte na blokadzie, ale wymagają starannej implementacji, aby uzyskać prawidłowe działanie. I oczywiście wymagają natywnej atomowości sprzętowej zapisów / obciążeń wskaźników; jeśli twoja atomic<>implementacja używa blokady wewnętrznie, jesteś prawie skazany.

Podobnie, jeśli masz kilku czytelników i / lub pisarzy, jesteś prawie skazany: możesz spróbować wymyślić schemat bez blokady, ale wdrożenie go będzie trudne. Sytuacje te są znacznie łatwiejsze w obsłudze za pomocą zamka. Jednak, gdy chwycić blokadę można przestać się martwić o new/ deletewydajności.

cmaster - przywróć monikę
źródło
+1 Muszę sprawdzić to rozwiązanie bufora pierścieniowego jako alternatywę dla równoczesnych kolejek korzystających z pętli CAS. Brzmi bardzo obiecująco.