Fragmentacja pamięci to ta sama koncepcja, co fragmentacja dysku: odnosi się do marnowania miejsca, ponieważ używane obszary nie są wystarczająco blisko siebie upakowane.
Załóżmy, że dla prostego przykładu z zabawką masz dziesięć bajtów pamięci:
| | | | | | | | | | |
0 1 2 3 4 5 6 7 8 9
Teraz przydzielmy trzy trzy bajtowe bloki o nazwach A, B i C:
| A | A | A | B | B | B | C | C | C | |
0 1 2 3 4 5 6 7 8 9
Teraz cofnij przydział bloku B:
| A | A | A | | | | C | C | C | |
0 1 2 3 4 5 6 7 8 9
Co się stanie, jeśli spróbujemy przydzielić czterobajtowy blok D? Cóż, mamy cztery bajty wolnej pamięci, ale nie mamy czterech sąsiadujących bajtów wolnej pamięci, więc nie możemy przydzielić D! Jest to nieefektywne wykorzystanie pamięci, ponieważ powinniśmy móc przechowywać D, ale nie byliśmy w stanie. I nie możemy przesunąć C, aby zrobić miejsce, ponieważ bardzo prawdopodobne, że niektóre zmienne w naszym programie wskazują na C, i nie możemy automatycznie znaleźć i zmienić wszystkich tych wartości.
Skąd wiesz, że to problem? Cóż, największym znakiem jest to, że rozmiar pamięci wirtualnej twojego programu jest znacznie większy niż ilość pamięci, której faktycznie używasz. W prawdziwym przykładzie miałbyś dużo więcej niż dziesięć bajtów pamięci, więc D zostałby przydzielony zaczynając od bajtu 9, a bajty 3-5 pozostałyby nieużywane, chyba że później przydzielisz coś o długości trzech bajtów lub mniejszej.
W tym przykładzie 3 bajty to nie wiele do stracenia, ale rozważ bardziej patologiczny przypadek, w którym dwa przydziały pary bajtów to na przykład dziesięć megabajtów pamięci i trzeba przydzielić blok o wielkości 10 megabajtów + 1 bajt. Aby to zrobić, musisz poprosić system operacyjny o ponad dziesięć megabajtów więcej pamięci wirtualnej, nawet jeśli brakuje Ci już tylko jednego bajta.
Jak temu zapobiec? Najgorsze przypadki zdarzają się, gdy często tworzysz i niszczysz małe obiekty, ponieważ powoduje to efekt „szwajcarskiego sera” z wieloma małymi przedmiotami oddzielonymi przez wiele małych otworów, uniemożliwiając przydzielenie większych obiektów w tych otworach. Gdy wiesz, że zamierzasz to robić, skuteczną strategią jest wstępne przydzielenie dużego bloku pamięci jako puli dla małych obiektów, a następnie ręczne zarządzanie tworzeniem małych obiektów w tym bloku, zamiast pozwalania domyślny alokator obsługuje to.
Ogólnie rzecz biorąc, im mniej alokacji, tym mniej prawdopodobne jest rozdrobnienie pamięci. Jednak STL radzi sobie z tym dość skutecznie. Jeśli masz ciąg znaków, który korzysta z całej jego bieżącej alokacji i dołączysz do niego jeden znak, nie po prostu ponownie alokuje do swojej bieżącej długości plus jeden, podwaja jego długość. Jest to odmiana strategii „pula częstych małych przydziałów”. Łańcuch chwyta dużą część pamięci, dzięki czemu może skutecznie radzić sobie z powtarzającymi się niewielkimi wzrostami rozmiaru bez powtarzających się małych przesunięć. Wszystkie kontenery STL w rzeczywistości robią takie rzeczy, więc ogólnie nie musisz się zbytnio martwić fragmentacją spowodowaną automatycznym realokowaniem kontenerów STL.
Choć oczywiście nie kontenery STL puli pamięci między sobą, więc jeśli masz zamiar stworzyć wiele małych pojemników (zamiast kilku pojemników, które się często zmieniany) może trzeba martwić się o zapobieganie fragmentacji w taki sam sposób, w jaki będzie dla każdego często tworzonego małego obiektu, STL lub nie.