Kontrolowanie dostępnej pamięci przy używaniu SetLength

0

Czasem przy używaniu procedury SetLength w pętli jest tak dużo okrążeń, że w pewnym momencie program się wysypuje i dostaję komunikat

Project raised exception class EOutOfMemory with message 'Out of memory'

Jak to zbadać przed wywołaniem SetLength?

2

Zadbać w sensie? Co dokładnie robisz i faktycznie ile tej pamięci próbujesz wyhaczyć?
Możesz zrobić coś w stylu:

  try
    SetLength(S, NewSize);
  except
    on E: EOutOfMemory do
      //o cholera brak pamięci tu odpowiednie działania
  end;

Aha ze względu na specyfikę działania procedury SetLength zawsze musisz mieć dostępne przynajmniej tyle wolnej pamięci aby wystarczyło na całą nową wielkość stringa czy tam tablicy (nie tylko tyle o ile chcesz zwiększyć).

0

Czy możesz wrzucić tu fragment kodu, który powoduje problem? Napisz też proszę, jaka wersja Delphi oraz Windowsa.

4
ihuarraquax napisał(a):

Czasem przy używaniu procedury SetLength w pętli […]

SetLength nie używa się w pętli, bo to najgorsze możliwe rozwiązanie, biorąc pod uwagę wydajność. Zmiana rozmiaru macierzy oznacza relokację bieżącego bloku pamięci do nowego miejsca, jednocześnie alokując dodakowo blok o rozmiarze będącym różnicą rozmiaru źródłowego i docelowego.

Tak więc dla większych macierzy, SetLength jest wąskim gardłem, dlatego np. listy alokują pamięć z zapasem – im więcej elementów w liście, tym większy zapas. Rozmiar listy z owym zapasem można sprawdzić za pomocą właściwości Capacity.

Przy pomniejszaniu rozmiaru macierzy, relokacja pamięci najprawdopodobniej nie zachodzi, bo nie ma takiej potrzeby. Natomiast jeśli chodzi o ciągi znaków, to tutaj sytuacja nie jest taka oczywista, bo w grę wchodzi dodatkowo refcountowanie.


Tak więc jeśli operujesz na dużym zbiorze danych, skorzystaj z jakiejkolwiek listy (np. generycznej), bo jej wydajność jest dużo lepsza, a poza tym, ma wbudowanych wiele właściwości i metod ułatwiających operowanie na danych.

Zastanów się też nad samym algorytmem, bo coś mi się wydaje, że za dużo danych ładujesz do pamięci. Obstawiam, że wyjątek ten nie dotyczy pracy programu na 50-letnim sprzęcie, więc zapchanie całego RAM-u (plus ew. pliku stron) wygląda podejrzanie. Jeśli coś nie musi istnieć w pamięci, to tego nie twórz – ładuj tylko wtedy, gdy to naprawdę konieczne.

Edit: zabezpieczenia przed niemożnością alokacji wymaganej pamięci oczywiście istnieć powinny, dlatego że program może być uruchomiony w środowisku bardzo obciążonym, z niewielką ilością wolnej pamięci.

0

Odpisuję na razie na szybko, bardziej żeby dać znać, że wciąż jestem zainteresowany, bo do tematu będę mógł wrócić dopiero po weekendzie.

Delphi jest 7, Windows 10 Home.

Fragment kodu to

type TablicaType=record ile:integer; T:array of string; end;

procedure DodajDoTablicy(wyraz:string; var K:TablicaType);
begin 
      Inc(K.ile);
      if K.ile>High(K.T) then SetLength(K.T,High(K.T)+porcja);
      K.T[K.ile]:=wyraz;
end;

Stała "porcja" jest dość duża, więc przy większości uruchomień wystarcza jedno startowe SetLength na początku, a kolejne powiększenia tablicy odbywają się skokami, a nie przy każdym okrążeniu. Po prostu przy szczególnych konfiguracjach danych czasem wychodzi tych kombinacji nadzwyczajnie dużo. Te przypadki mógłbym właściwie sobie odpuścić, bo ich wartość wynikowa jest wątpliwa, ale nie potrafię uchwycić tego momentu, kiedy już jest za dużo, a jeszcze za mało.

Dodałem tam "try", jak wyżej, ale nie pomogło. Owszem, wykonało się to, co miało się wykonać, ale... nie przy każdym uruchomieniu programu. Czasem zadziałało (rzadko), a czasem wywalało się po staremu.

Miałem nadzieję, że istnieje jakaś prosta funkcja zwracająca wielkość dostępnej pamięci, którą można by zbadać to przed wywołaniem SetLength.

Moja orientacja w Delphi jest nieoczywista, jestem samoukiem i na przykład do "try" podchodzę bez powodzenia po raz już któryś. O liście generycznej nie słyszałem, będę sobie musiał doczytać. Ale to już muszę odłożyć na po weekendzie. Dziękuję za odzew.

1

Masz jakiś sensowny powód użycia typu TablicaType? Czy 'ile' z rekordu wykorzystujesz gdzieś jeszcze czy tylko do ustalania rozmiaru tablicy T (trochę niefortunna nazwa...)? Nie prościej byłoby wrzucać te stringi do TStringList?

1

O jejku, Delphi 7… Fakt, w tej wersji nie ma jeszcze generyków, więc pozostaje albo skorzystać z istniejących kontenerów, albo napisać jakiś swój. Przy czym w dalszym ciągu korzystanie ze zwykłej macierzy jest nieco masochistyczne… ;)

Twoja procedura DodajDoTablicy w sposób nieintuicyjny dodaje elementy do tablicy. Jeśli indeks elementu jest większy od ostatniego to macierz zostaje powiększona, ale jeśli nie, to zawartość istniejącej komórki zostaje nadpisana. Ta procedura łączy w sobie dwie rzeczy, istniejące choćby w TStringList – dla indeksu istniejącej komórki będzie to odpowiednik mutatora właściwości Items, a dla większego, odpowiednik metody Add. Bardzo to dziwne.


Jedyne czego potrzebujesz to przechowywać ciągi znaków, a w tej wersji Delphi masz do dyspozycji klasę TStringList, więc z niej powinieneś skorzystać. Ręczna implementacja czegoś podobnego nie ma sensu, tym bardziej w postaci gołych, globalnych procedur i funkcji.

Jedyny sens własnej implementacji widziałbym w przypadku, gdy lepszym rozwiązaniem od tablicowego kontenera byłby ten bazujący wewnętrznie na liście jedno- lub dwukierunkowej, bo takich w bibliotece standardowej nie ma. W razie czego możesz wykorzystać kod listy dwukierunkowej, który opisałem w artykule pt. Implementacja listy dwukierunkowej ze znacznikiem. Działa dużo szybciej od tradycyjnej listy dwukierunkowej, za sprawą zapamiętywania ostatnio używanego węzła (znacznika).

0

Typ TablicaType został tu okrojony na potrzeby przykładu, najczęściej mam tam więcej pól. Dzięki za sugestię nt. TStringList.

List dwukierunkowych używałem kiedyś bardzo dawno temu, potem przestawiłem się na sposób jak powyżej. Nie pamiętam już dlaczego, ale miało to jakiś związek z przesiadką z TP na Delphi. W każdym razie sądziłem, że listy dwukierunkowe to coś stosownego na czasy, kiedy nie było jeszcze tablic dynamicznych, ale tym artykułem zmieniło mi optykę. Chętnie się w to zagłębię, skoro jest sygnał, że ma to sens, bo nigdy się nie przekonałem tak do końca do arrays of string, zawsze coś mnie w nich odstręczało (te kilka banalnych linijek kodu zawsze kopiuję i ile razy bym tego nie robił, a robiłem setki razy, to z głowy nie odtworzę, bo nie lubię).

Napisałeś, że listy dwukierunkowe nie potrzebują ciągłego bloku pamięci i dlatego mogą zawierać dowolną ilość węzłów. Czy to jedna z różnic pomiędzy nimi a arrays of string i dlatego tak łatwo pakowałem się na OutOfMemory?

0
ihuarraquax napisał(a):

W każdym razie sądziłem, że listy dwukierunkowe to coś stosownego na czasy, kiedy nie było jeszcze tablic dynamicznych, ale tym artykułem zmieniło mi optykę.

To zależy – w TP co prawda nie było typowych macierzy dynamicznych, ale i tak były funkcje umożliwiające dynamiczną alokację pamięci. Mowa oczywiście o GetMem, AllocMem, ReallocMem i FreeMem. Tyle że aby móc z nich wygodnie korzystać, należało je sobie w ”coś” opakować (np. w object, czyli w odpowiednik dzisiejszych zaawansowanych rekordów, bo klas jeszcze nie było).

Chętnie się w to zagłębię, skoro jest sygnał, że ma to sens […]

Listy dwukierunkowe mają sens, przede wszystkim wtedy, gdy dużo i często dodajemy do kontenera lub z niego usuwamy. Brak realokacji dużych bloków pamięci znacznie skraca operacje modyfikacji listy.

Napisałeś, że listy dwukierunkowe nie potrzebują ciągłego bloku pamięci i dlatego mogą zawierać dowolną ilość węzłów.

Tak, macierze zawsze alokowane są jako jeden ciągły blok pamięci, natomiast w przypadku list nie ma takiego ograniczenia. To sprawia, że dodanie węzła do listy oznacza jedynie alokację pamięci dla nowego węzła – nic więcej.

Lista, którą opracowałem i przedstawiłem w artykule, bazuje na konstrukcji standardowej listy dwukierunkowej. Dodawanie i usuwanie węzłów jest szybkie – to dla list standard. Natomiast słabym punktem jest iterowanie po kolejnych węzłach – edycja tego konkretnego oznacza jego uprzednie odszukanie, czyli przeiterowanie po wszystkich węzłach, od głowy lub ogona. Dlatego też swoją listę podrasowałem pod tym kątem, dodając węzeł-znacznik – ten zawsze zawiera wskaźnik ostatnio użytego węzła. To znacznie skraca czas przeszukiwania listy.

Oczywiście konstrukcja listy dwukierunkowej opakowana jest w wygodną klasę, dzięki czemu nie trzeba się samemu babrać wskaźnikami i dbać o jej poprawną budowę. Używa się jej identycznie jak TObjectList – w środku dzieje się magia ze wskaźnikami, a na wierzchu operuje się na indeksach. Choć przydałoby się ją przerobić na generyczną…

Czy to jedna z różnic pomiędzy nimi a arrays of string i dlatego tak łatwo pakowałem się na OutOfMemory?

Na pewno ma to wpływ. Zwróć uwagę na to, że im większa macierz, tym większy blok pamięci trzeba zarezerwować w całości. Menedżer pamięci nie działa jak defragmentator – alokuje pamięć tam gdzie mu wygodnie, a nie po kolei. To sprawia, że pamięć jest poszatkowana – trochę zajętej, trochę wolnej, znów trochę zajętej i znów wolna dziura. W tak podziurawionej pamięci w pewnym momencie nie da się zaalokować wystarczająco dużego bloku – pomimo tego, że łącznie może być tej wolnej o wiele więcej, niż to wymagane.

Listy dają tu przewagę – każda najmniejsza wolna przestrzeń może być zagospodarowana. Czyli każdy blok o rozmiarze większym lub równym rozmiarowi pojedynczego węzła (dwa wskaźniki plus dane). Dzięki temu da się stworzyć o wiele więcej węzłów, bo można je poupychać w każdą wolną lukę w pamięci.

Oczywiście listy mają też swoje wady, jednak coś za coś.

1 użytkowników online, w tym zalogowanych: 0, gości: 1