Zarządzanie pamięcią - przeładowane operatory new i delete

Witam!

Miałem zrobić takie ćwiczenie pod koniec działu o przeładowanych operatorach:

Prosty program, ale odnośnie jednej rzeczy mam pytanie. Kiedy przekroczymy rezerwację, tzn. wykorzystaliśmy już 100 obiektów to teraz nie pozostaje nic innego niż ustawienie składnika statycznego ktory_raz na 0. I tutaj jest właśnie wg mnie minus tego programu - jeżeli pierwszy wskaźnik miał nazwę wektor_sily, potem w programie wykorzystaliśmy te 100 obiektów i zaczynamy od zera to teraz jak zdefiniujemy wskaźnik predkosc i będziemy chcieli usunąć delet’em - albo 1-szy albo 2-gi - to tylko jeden. Usunięcie jednego spowoduje usunięcie drugiego. Tego minusu nie da się chyba naprawić.

A odnośnie programu to powiedzcie co można było by w nim zmienić.

#include 

using namespace std;


//////////////////////////////////////////////////

class wektor

{

	int x, y;

	static int *wsk[100];

	static int ktory_raz;


public:

	//------------konstruktor

	wektor(int a = 0, int b = 0) : x(a), y(b)

	{ }


	//###################################################

	//############# PRZEŁADOWANE OPERATORY ##############

	//---------------------------------------------------

	//---------------------------------------------------

	static void* operator new(size_t rozmiar)

	{

		if(ktory_raz == 0)

		{

			for(int i = 0; i < 100 ; i++)

			{

				wsk[i] = new int[rozmiar];

			}

			return wsk[ktory_raz++];

		}

		else

		{

			if(ktory_raz == 100)	//jeżeli zapełniliśmy całą tablicę wskaźników

			{ //to zaczynamy od początku (ktory_raz = 0)

									//odbędzie się nadpisanie !

				ktory_raz = 0;		

			}

			return wsk[ktory_raz++];

		}

	}

	//---------------------------------------------------

	//---------------------------------------------------

	static void operator delete(void *do_skasowania)

	{

		if(ktory_raz == 100)

		{

			for(int i = 0 ; i < 100 ; i++)	//jeżeli ktory_raz jest równy 100

				delete wsk[i]; //to zwalniamy całą rezerwację

		}	

		else

		{

			int ktory_numer;

			for(int i = 0 ; i <= 100 ; i++)

			{

				ktory_numer = i;

				if(wsk[i] == do_skasowania) //musimy wyszukać w naszej rezerwacji adres

				{ //obiektu na rzecz, którego chcemy wykonać 

					break; //zwolnienia pamięci											

				}

			}

			delete wsk[ktory_numer];

		}

	}

	//---------------------------------------------------

	//---------------------------------------------------

};

//////////////////////////////////////////////////


int* wektor::wsk[100];	//definicje obiektów statycznych klasy 'wektor'

int wektor::ktory_raz;


int main()

{


	wektor* wektor_sily; //ktory_raz = 0

	wektor_sily = new wektor;


	wektor* wektor_predkosci;	//ktory_raz = 1

	wektor_predkosci = new wektor;


	wektor* tabl[98]; //ktory_raz = 99

	for(int i = 0 ; i < 98 ; i++)

	{

		tabl[i] = new wektor;

	}


	//ktory_raz wynosi: 100

	wektor* predkosc = new wektor;


	//ktory_raz wynosi: 1


	delete wektor_sily; //i teraz się skasuje wsk[0]

	//delete predkosc; <----- no i teraz byłby błąd - o tym mówiłem właśnie

}

Z góry jak zwykle - dziękuję za pomocne rady.

Autorowi zadania na pewno nie chodziło o zagadnienie przekroczenia ilości 100 obiektów, zresztą jak to zadanie rozwiążesz porządnie to samo wyjdzie co robić w przypadku jak ilość obiektów przekroczy 100.

Zastanów się poco przydziela się pamięć od razu na 100 obiektów? Ano po to że czas na przydzielenie jednego kawałku długością 8 bajtów jest taki sam jak przydzielenie jednego kawałku długością 800 bajtów. Oprócz tego jeżeli klasa zawiera mniej niż 8 bajtów to można sporo oszczędzić (jak przydzielasz 2 bajty to i tak zajmujesz 8 bajtów). Z powyższego wynika że przydzielać pamięć masz jedną instrukcją new. Do operatora new zostaje przekazany rozmiar w bajtach, wiec kiedy przydzielasz wsk = new int[rozmiar]; to przydzielasz pamięci na 4 obiekty. Absolutnie nie uwzględniasz że po przydzieleniu obiektów A,B,C obiekt B może zostać zwolniony, zaś w zadaniu jest wyraźnie powiedziano że masz to uwzględnić.

Aha, no to tego nie podejrzewałem…

Czyli jak :?:

wsk = new int[rozmiar * 100];

:?:

Jak na cztery - jak już to na jeden przecież - na cztery to by było rozmiar * 4 wg mnie…

Nie.

wsk=new char[rozmiar*100]; [/code]

int ma rozmiar 4 bajty, przydzielasz 4*100*rozmiar bajtów, a trzeba 100*rozmiar.

No ale jak się posłużyć takim wskaźnikiem przy zwracaniu rezultatu :?: Mam wskaźnik o wielkości 800 bajtów i jak tu np. zrobić, żeby za każdym razem przeskakiwał o 8 bajtów (tyle ma obiekt klasy) i zwracał ten adres ? Myślę, że tak, prawda :?:

return wsk + 8;

zamiast 8 może być jakaś zmienna jeszcze się nie zastanawiałem nad tym. Ale takie zwracanie jest poprawne, zgadza się :?:

wektor *wsk=(wektor*)new char[100*rozmiar]; [/code]

Sugerowałbym też użycie tablicy bitów, która by wskazywała który ze 100 obszarów jest aktualnie wolny a który zajęty.

Przy próbie przydzielenia ponad 100 elementów albo przydzielasz kolejny blok na 100 elementów albo zwracasz 0 czyli brak pamięci.

Obiekt klasy wektor powinien posiadać informację o tym, do którego bloku pamięci przynależy, zajmowana przez niego pamięć. Obiekt ten powinien przechowywać pole, wskazujące na początek zarezerwowanego bloku pamięci. Informacja ta jest potrzebna do tego, aby było możliwe zwolnienie prawidłowego bloku pamięci, przeciążonym operatorem delete. Zwróć uwagę, że obiekt może mieć przydzieloną pamięć w bloku, który nie jest już wykorzystywany do nowych alokacji. Blok ten może nie być ostatnio używanym blokiem, który jest wskazywany przez statyczny wskaźnik i trzeba umieć go zwolnić.

Przeciążony operator new, powinien alokować pamięć jednorazowo dla całej puli (100) obiektów a nie dla każdego obiektu z osobna.

wektor* bufor = reinterpret_cast(new char[sizeof(wektor) * 100]);

Przeciążona funkcja operatora new, posiada parametr typu size_t, niosący informację o wielkości przydzielanej pamięci. Jest to ilość pamięci potrzebnej dla obiektu danego typu, wyrażona w bajtach. Z parametru tego nie trzeba korzystać, bo można go wyznaczyć statycznie. W Twoim przypadku jest on równy sizeof(wektor). Poniżej zamieszczam przykład kodu, implementujący uproszczony mechanizm puli obiektów.

template

class pula_obiektow

{

	struct bufor_pamieci

	{

		bufor_pamieci() 

			: licznik_przydzielonych_obiektow(0), licznik_zwolnionych_obiektow(0) {

		}


		void* przydziel_pamiec_dla_obiektu() { 

			return bufor + licznik_przydzielonych_obiektow++ * sizeof(TypObiektu); 

		}


		void zwolnij_pamiec_obiektu() { 

			++licznik_zwolnionych_obiektow; 

		}


		bool jest_pelny() const { 

			return licznik_przydzielonych_obiektow == IloscObiektowWBuforze; 

		}


		bool gotowy_do_zwolnienia() const { 

			return licznik_zwolnionych_obiektow == IloscObiektowWBuforze; 

		}


	private:

		int licznik_przydzielonych_obiektow;

		int licznik_zwolnionych_obiektow;

		char bufor[IloscObiektowWBuforze * sizeof(TypObiektu)];

	};


public:

	static void* operator new(size_t) 

	{

		if (ostatni_bufor_pamieci() == 0) {

			ostatni_bufor_pamieci() = ::new bufor_pamieci;

		}


		TypObiektu* obiekt = static_cast(ostatni_bufor_pamieci()->przydziel_pamiec_dla_obiektu());

		obiekt->uzyty_bufor_pamieci = ostatni_bufor_pamieci(); 


		if (ostatni_bufor_pamieci()->jest_pelny()) {

			ostatni_bufor_pamieci() = 0;

		}


		return obiekt;

	}


	static void operator delete(void* pointer) 

	{

		TypObiektu* obiekt = static_cast(pointer);


		obiekt->uzyty_bufor_pamieci->zwolnij_pamiec_obiektu();


		if (obiekt->uzyty_bufor_pamieci->gotowy_do_zwolnienia()) {

			delete obiekt->uzyty_bufor_pamieci;

		}

	}


private:

	bufor_pamieci* uzyty_bufor_pamieci;


	static bufor_pamieci*& ostatni_bufor_pamieci() 

	{

		static bufor_pamieci* ostatni_bufor_pamieci = 0;

		return ostatni_bufor_pamieci;

	}

};


class wektor : public pula_obiektow

{

public:

	wektor(int a = 0, int b = 0) 

		: x(a), y(b) {

	}


	int x, y;

};

To nie do końca jest prawdą. Typowa, minimalna wielkość bloku, alokowana globalnym operatorem new to 64 bajty. Tutaj faktycznie, czas alokacji tablicy 2 bajtów jest taki sam, jak czas alokacji tablicy 64 bajtów i można wiele zyskać na skorzystaniu z puli obiektów.

W przypadku, gdy alokowany obszar pamięci jest większy od 64 bajtów, czas alokacji jest tym dłuższy im więcej pamięci trzeba przydzielić. W takim przypadku, zysk z puli obiektów będzie niewielki a często nawet żaden.

Zastanawiam się skąd czerpiesz tę rozbieżne z rzeczywistością informacje?

#include using namespace std;

Nie porównuj groch z kapustą. W jednym przypadku przydzielasz w sumie 1MB a w drugim 1GB. Tacy jak ty udowodnili że zdolności matematyczne wprost proporcjonalne długości stopy. Do grupy doświadczalnej brali wszystkich w tym niemowląt, które oczywiście wykazywali zerowe zdolności matematyczne.

W jednym i drugim przypadku wykonałem milion alokacji, czyż nie?

Sam napisałeś, że rozmiar przydzielanej pamięci nie ma znaczenia.

Przykład który przedstawiłem i tak jest lajtowy, bo prawdziwą różnicę w szybkości alokacji widać dopiero wtedy, gdy sterta jest mocno pofragmentowana.

To rób porównania dla przydzielania np po 32 i 128 aby całkowity rozmiar przydzielonej pamięci nie różnił się drastycznie, albo rób przydzielenie i zwolnienie obszaru na 1B milion razy a potem przydzielenie i zwolnienie obszaru na 1KB milion razy. Kiedy próbujesz przydzielić 1GB to przeważnie zaczyna się swapowanie pamięci a to jest oczywiście kosztowna operacja. Wystarczy jak przestawisz te dwa testy miejscami a dostaniesz zupełnie inny wynik.

Nie analizowałem niestety Twojego kodu pebal bo nie czytałem o dziedziczeniu i szablonach…

Chyba już wykombinowałem :smiley: Po dwóch godzinach… W main są przykładowe użycia i rezultaty

#include 

using namespace std;


///////////////////////////////////////////////////////////////////////////////////////

class wektor

{

public: 

	//Składniki są public bo niektóre potrzebuję do main na rzecz tego przykładu

	//potem można je oznaczyć jako private (a nawet powinno się)

	int x, y;

	static char *wsk;

	static int ile_przeskoczyc;

	static int size; 

	static bool czy_bylo_kasowanie;

	static void* miejsce_po_kasowaniu;


public:

	//------------konstruktor

	wektor(int a = 0, int b = 0) : x(a), y(b)

	{ }


	static void* operator new(size_t rozmiar);

	static void operator delete(void *co_skasowac);

	static bool zwolniony_obiekt(int wersja, void* wolne_miejsce = 0);


};

///////////////////////////////////////////////////////////////////////////////////////

char* wektor::wsk = 0;

int wektor::ile_przeskoczyc = 0;

int wektor::size = sizeof(wektor);	//(dzięki tej zmiennej nasze operatory zadziałają w każdej klasie)

void* wektor::miejsce_po_kasowaniu = 0;

bool wektor::czy_bylo_kasowanie = false;

// *************************************************************************************

void* wektor::operator new(size_t rozmiar)

{

	static int czy_pierwszy_raz = true;

	if(czy_pierwszy_raz)

	{

		wsk = new char[rozmiar * 100];

		czy_pierwszy_raz = false;

		return wsk;	//obiekt zbudowany zostanie na pierwszych 8 bajtach z 800

	}

	else //jeżeli to już drugi raz tworzymy coś new'em

	{

		if(zwolniony_obiekt(2)) //jeżeli przed chwilą coś skasowaliśmy to zwolni się miejsce

		{

			czy_bylo_kasowanie = false;

			return miejsce_po_kasowaniu;

		}


		ile_przeskoczyc += size;

		if(ile_przeskoczyc == 800) //zaczynamy zabawę od początku

		{

			ile_przeskoczyc = 0;

			wsk = new char[rozmiar * 100];

			return wsk;

		}

		else

		{

			return wsk + ile_przeskoczyc;

		}

	}

}

// *************************************************************************************

void wektor::operator delete(void *co_skasowac)

{

	if(ile_przeskoczyc == 792)

		delete wsk; //cała rezerwacja - sizeof(klasa) * 100

	else

	{

		zwolniony_obiekt(1, co_skasowac);

	}

}

// *************************************************************************************

bool wektor::zwolniony_obiekt(int wersja, void* wolne_miejsce)

{

	if(wersja == 1) //jeżeli wywołaliśmy z operatora delete

	{

		czy_bylo_kasowanie = true;

		miejsce_po_kasowaniu = wolne_miejsce;

		return 0; //trzeba coś zwrócić, ale tak na prawdę przyda się to w "etykiecie" 2

	}

	if(wersja == 2) //jeżeli wywołaliśmy z operatora new

	{

		return czy_bylo_kasowanie;

	}

}

// *************************************************************************************


int main()

{

	wektor* tabl[100];

	for(int i = 0 ; i < 100 ; i++)

		tabl[i] = new wektor;

	cout << "Ile przeskoczyc - " << wektor::ile_przeskoczyc << "\n";	//792


	wektor* tablica[50];

	for(int i = 0 ; i < 50 ; i++)

		tabl[i] = new wektor;

	cout << "Ile przeskoczyc - " << wektor::ile_przeskoczyc << "\n";	//392


	delete tabl[45];


	cout << "Ile przeskoczyc - " << wektor::ile_przeskoczyc << "\n";	//392


	//######################## DRUGI PRZYPADEK ########################

	/*

	wektor* w1 = new wektor;

	cout << "Ile przeskoczyc - " << wektor::ile_przeskoczyc << "\n";	//0

	wektor* w2 = new wektor;

	cout << "Ile przeskoczyc - " << wektor::ile_przeskoczyc << "\n";	//8

	wektor* w3 = new wektor;

	cout << "Ile przeskoczyc - " << wektor::ile_przeskoczyc << "\n";	//16

	delete w1;

	wektor* w4 = new wektor;

	cout << "Ile przeskoczyc - " << wektor::ile_przeskoczyc << "\n";	//dalej 16

	*/


}

Co o tym sądzisz alex :?:

Poniższa pętla:

for (int x = 0; x < 10000000; ++x) {

     new wektor;

}

W przypadku standardowej alokacji:

czas 711ms, pamięć 155MB;

W przypadku alokacji z wykorzystaniem puli 256 obiektów:

czas 257ms, pamięć 165MB;

Wykorzystanie puli skraca czas zaledwie trzykrotnie, przy podobnej zajętości pamięci. Dodatkowo trzeba pamiętać, że pula nie jest thread-safe i nie można z niej wszędzie skorzystać.

Skoro mierzysz czas w milisekundach to wynik ten może być mocno zafałszowany, odpal to jeszcze raz i zobacz jakie będą czasy. Windowsy mogli akurat w czasie jednego z tych odpaleń akurat sprawdzać czy nie ma jakieś aktualizacji, albo coś w tym guście. Przynajmniej zrób 10 tys takich uruchomień i oblicz średnie po wyrzuceniu powiedzmy 1000 najmniejszych i 1000 największych czasów. Powiedz mi czemu ludzi nie mające zielonego pojęcia o statystyce tak lubią w statystykę się bawić? Dla mnie ten twój argument przypomina dowód jednego dzieciaka: Ponieważ przy mnożeniu zawsze wychodzi więcej np (2+3=5, 2*3=6) a 2+2=4 to 2*2 ma być większa od 4.

Żenada.

Przedstawiłem średnią z dziesięciu prób, więc sobie daruj. Mój komp ma 4GB RAM i 4 rdzenie, więc pomiary były raczej stabilne. Alokowanie pamięci dużymi blokami pamięci, wcale nie daje takich wspaniałych efektów, jak się to może wydawać.

Jeżeli podważasz wiarygodność przedstawionych przeze mnie danych, to przeprowadź sobie testy samemu. I nie czaruj, że masz takie duże pojęcie o statystyce, bo z Twojej wypowiedzi wynika, że jest wręcz odwrotnie.

Ej a odnośnie mojego kodu :?: Jest on poprawny, co byście w nim zmienili :?:

(3 post od dołu na 1-szej stronie…)

Przeprowadziłem testy statystyczne, dla kilku komputerów.

Średni zysk to trochę ponad połowa wielokrotności przydzielenia.

Czyli np obiekty po 3 bajty przydzielam po 100 szt zysk w czasie 52 krotny.

Opowiem ci kawał:

Siedzi zając na pniu czyta książkę. Zobaczył to wilk i pyta zająca: - co czytasz?

Zając: - Książkę, “Logika” się nazywa.

Wilk: - A co to jest ta logika?

Zając: - Może ci na przykładzie wytłumaczę, masz zapałki?

Wilk: - Mam.

Zając: - No więc logicznie wnioskując … Skoro masz zapałki to palisz. Skoro palisz to też pijesz. Skoro pijesz i palisz to często na imprezach się kręcisz. Skoro na imprezach się kręcisz to często z dziewczynami baraszkujesz.

Wilk: - Fajna książka, dawaj tu.

I zabrał zającowi książkę, usiadł i zaczął czytać. Idzie Niedźwiedź, zobaczył wilka i pyta: - co czytasz?

Wilk: - Książkę, “Logika” się nazywa.

Niedźwiedź: - A co to jest ta logika?

Wilk: - Może ci na przykładzie wytłumaczę, masz zapałki?

Niedźwiedź: - Nie mam.

Wilk: - No więc logicznie wnioskując … Jesteś piedziem.

Logikę masz dokładnie na poziomie tego wilka.

Quentyn - spójrz uważnie na swój kod, np druga tablica w main() zadeklarowałeś a dokąd wpisujesz przydzielone obiekty?

Do każdego bloku po 100 obiektów musisz dodać tablicę bitowa (można tablicę bool, jak nie chcesz z bitami walczyć) gdzie będzie zapisane który blok jest zajęty (0/false-wolny,1/true-zajęty). Jak przydzielasz to szukasz pierwszy z brzegu wolny zaznaczasz jako zajęty i zwracasz adres. Jak zwalniasz to wyliczasz które miejsce się zwolniło i zaznaczasz że jest wolne.

Na jednej zmiennej nie da rady zrobić poprawne zarządzanie stertą pamięci. Co będzie w twoim kodzie jeżeli po przydzieleniu 100 obiektów parzystę zostaną zwolnione, potem zostaną przydzielone 50 obiektów do innej tablicy, potem zwolnione nieparzyste z pierwszej i na koniec przydzielone jeszcze 50 obiektów do drugiej tablicy?

Nie działa to jak należy i nie może przy takim podejściu.

A tych tablic ma być określona ilość czy one mają być tworzone też tak automatycznie jak miejsce pod 100 obiektów :?: Tzn. jest tworzone nowe 800 bajtowe miejsce w zapasie pamięci - jest tworzona nowa tablica. Wg mnie to się nie uda i chyba trzeba stworzyć tych tablic określoną ilość, co :?:

Nie zupełne.

Po pierwsze warto zmienić te N=100 na N=128 lub N=64 (sugeruje 64), czemu? … czytaj dalej.

Po drugie każdy z obszarów 8*N powinien zawierać N bitów (jeżeli N==64 to jedna zmienna unsigned long long) lub też tablica na bool na N elementów na zaznaczenie jaka część jest zajęta, wskaźnik na następny taki obszar (dodatkowy wskaźnik na poprzedni obszar uprości zwolnienie całego obszaru).

Po trzecie dwie statyczne zmienne dla całej klasy wskaźnik na pierwszy obszar (nieco uprości usuwanie obszarów dodatkowy wskaźnik na ostatni obszar)