Obracam niektóre z najbardziej centralnych części mojej bazy kodu (silnik ECS) wokół opisanej przez ciebie struktury danych, chociaż wykorzystuje mniejsze ciągłe bloki (bardziej jak 4 kilobajty zamiast 4 megabajtów).

Używa podwójnej wolnej listy w celu uzyskania ciągłego wstawiania i usuwania z jedną wolną listą dla wolnych bloków, które są gotowe do wstawienia (bloki, które nie są pełne) i pod-wolną listą wewnątrz bloku dla indeksów w tym bloku gotowy do odzyskania po włożeniu.
Omówię zalety i wady tej struktury. Zacznijmy od kilku wad, ponieważ jest ich kilka:
Cons
- Wstawienie do tej struktury kilkuset milionów elementów zajmuje około 4 razy więcej niż
std::vector(struktura czysto ciągła). I jestem całkiem przyzwoity w mikrooptymalizacjach, ale jest tylko koncepcyjnie więcej pracy do wykonania, ponieważ zwykły przypadek musi najpierw sprawdzić wolny blok na górze listy wolnych bloków, a następnie uzyskać dostęp do bloku i otworzyć wolny indeks z bloku wolna lista, napisz element w wolnej pozycji, a następnie sprawdź, czy blok jest pełny, i usuń blok z listy wolnych bloków, jeśli tak jest. Nadal jest to operacja o stałym czasie, ale ze znacznie większą stałą niż cofanie się std::vector.
- Zajmuje to około dwa razy więcej czasu podczas uzyskiwania dostępu do elementów przy użyciu wzorca losowego dostępu, biorąc pod uwagę dodatkową arytmetykę indeksowania i dodatkową warstwę pośredniczącą.
- Dostęp sekwencyjny nie jest skutecznie odwzorowywany na projekt iteratora, ponieważ iterator musi wykonywać dodatkowe rozgałęzienia za każdym razem, gdy jest zwiększany.
- Ma trochę narzutu pamięci, zwykle około 1 bit na element. 1 bit na element może nie brzmieć dużo, ale jeśli używasz go do przechowywania miliona 16-bitowych liczb całkowitych, oznacza to 6,25% więcej wykorzystania pamięci niż idealnie kompaktowa tablica. Jednak w praktyce zużywa to mniej pamięci, niż w
std::vectorprzypadku kompaktowania w vectorcelu wyeliminowania rezerwy nadmiaru. Poza tym generalnie nie używam go do przechowywania takich drobnych elementów.
Plusy
- Dostęp sekwencyjny za pomocą
for_eachfunkcji, która przyjmuje zakresy przetwarzania elementów zwrotnych elementów w bloku, prawie rywalizuje z szybkością dostępu sekwencyjnego std::vector(tylko jak 10% różnicy), więc nie jest to wcale mniej wydajne w najbardziej krytycznych dla mnie przypadkach użycia ( większość czasu spędzonego w silniku ECS ma dostęp sekwencyjny).
- Umożliwia ciągłe usuwanie ze środka, a struktura zwalnia bloki, gdy stają się całkowicie puste. W rezultacie jest to całkiem przyzwoite, aby upewnić się, że struktura danych nigdy nie zużywa znacznie więcej pamięci niż to konieczne.
- Nie unieważnia wskaźników dla elementów, które nie są bezpośrednio usuwane z kontenera, ponieważ pozostawia po sobie dziury, stosując metodę z bezpłatną listą, aby odzyskać te dziury po kolejnym wstawieniu.
- Nie musisz się tak bardzo przejmować brakiem pamięci, nawet jeśli ta struktura zawiera imponującą liczbę elementów, ponieważ żąda tylko niewielkich ciągłych bloków, które nie stanowią problemu dla systemu operacyjnego, aby znaleźć ogromną liczbę ciągłych nieużywanych strony.
- Dobrze nadaje się do współbieżności i bezpieczeństwa wątków bez blokowania całej struktury, ponieważ operacje są zazwyczaj lokalizowane w poszczególnych blokach.
Teraz jednym z największych plusów było dla mnie, że stworzenie niezmiennej wersji tej struktury danych stało się banalne:

Od tego czasu otworzyło to wiele drzwi do pisania większej liczby funkcji pozbawionych skutków ubocznych, co znacznie ułatwiło osiągnięcie bezpieczeństwa wyjątkowego, bezpieczeństwa wątków itp. Niezmienność była czymś, co odkryłem, co mogłem łatwo osiągnąć dzięki ta struktura danych z perspektywy czasu i przez przypadek, ale prawdopodobnie jedna z najpiękniejszych korzyści, jakie przyniosła, ponieważ znacznie ułatwiła utrzymanie bazy danych.
Nieciągłe tablice nie mają lokalizacji pamięci podręcznej, co powoduje złą wydajność. Jednak przy wielkości bloku 4M wydaje się, że będzie wystarczająca lokalizacja dla dobrego buforowania.
Lokalizacja odniesienia nie jest czymś, czym można się martwić w blokach tego rozmiaru, nie mówiąc już o blokach 4 kilobajtów. Linia pamięci podręcznej ma zwykle tylko 64 bajty. Jeśli chcesz zmniejszyć liczbę braków w pamięci podręcznej, po prostu skoncentruj się na odpowiednim wyrównaniu tych bloków i preferuj bardziej sekwencyjne wzorce dostępu, jeśli to możliwe.
Bardzo szybkim sposobem przekształcenia wzorca pamięci o dostępie swobodnym w sekwencyjny jest użycie zestawu bitów. Powiedzmy, że masz mnóstwo wskaźników i są one w losowej kolejności. Możesz po prostu przebijać je i oznaczać bity w zestawie bitów. Następnie możesz iterować po swoim zestawie bitów i sprawdzać, które bajty są niezerowe, sprawdzając, powiedzmy, 64-bitowe na raz. Po napotkaniu zestawu 64-bitów, w których ustawiono co najmniej jeden bit, można użyć instrukcji FFS, aby szybko ustalić, które bity są ustawione. Bity mówią ci, do których indeksów powinieneś uzyskać dostęp, chyba że teraz posortujesz indeksy w kolejności sekwencyjnej.
Ma to pewne koszty ogólne, ale w niektórych przypadkach może być opłacalną wymianą, szczególnie jeśli zamierzasz wielokrotnie zapętlać te indeksy.
Dostęp do przedmiotu nie jest tak prosty, istnieje dodatkowy poziom pośrednictwa. Czy zostanie to zoptymalizowane? Czy spowodowałoby to problemy z pamięcią podręczną?
Nie, nie można go zoptymalizować. Przynajmniej ten dostęp zawsze będzie kosztował więcej. Często jednak nie zwiększa to tak bardzo braków w pamięci podręcznej, ponieważ masz tendencję do uzyskiwania wysokiej lokalizacji czasowej z tablicą wskaźników do bloków, szczególnie jeśli twoje wspólne ścieżki wykonywania spraw używają wzorców sekwencyjnego dostępu.
Ponieważ po osiągnięciu limitu 4M następuje liniowy wzrost, możesz mieć o wiele więcej przydziałów niż zwykle (powiedzmy, maks. 250 przydziałów na 1 GB pamięci). Żadna dodatkowa pamięć nie jest kopiowana po 4M, jednak nie jestem pewien, czy dodatkowe alokacje są droższe niż kopiowanie dużych fragmentów pamięci.
W praktyce kopiowanie jest często szybsze, ponieważ jest to rzadki przypadek, występujący tylko jako log(N)/log(2)suma czasów, a jednocześnie upraszczający brudny zwykły przypadek, w którym można po prostu napisać element do tablicy wiele razy, zanim się zapełni i trzeba ją ponownie przydzielić. Tak więc zazwyczaj nie będziesz uzyskiwać szybszych wstawień przy tego typu strukturze, ponieważ zwykła praca z przypadkami jest droższa, nawet jeśli nie musi ona zajmować się tym drogim rzadkim przypadkiem realokacji dużych tablic.
Podstawową zaletą tej struktury dla mnie, pomimo wszystkich wad, jest zmniejszenie zużycia pamięci, nie martwienie się o OOM, możliwość przechowywania indeksów i wskaźników, które nie są unieważniane, współbieżności i niezmienności. Fajnie jest mieć strukturę danych, w której można wstawiać i usuwać rzeczy w stałym czasie, podczas gdy one same się oczyszczają i nie unieważniają wskaźników i wskaźników w strukturze.