Stos vs sterta - wskaźniki

0

Witam,
mam następujące pytania:

  1. czy każda zmienna utworzona w funkcji, ale bez pomocy operatora new będzie utworzona na stosie?
  2. Czy każda zmienna utworzona globalnie lub operatorem new będzie utworzona na stercie?
  3. Czy metoda (funkcja w klasie), w której są tworzone zmienne lokalne, będzie tworzyć je również na stosie - nawet, gdyby obiekt danej klasy był utworzony na stercie?
  4. Czy tworząc wskaźnik na stosie (w funkcji) do obiektu utworzonego na stercie (globalnie) inny obiekt utworzony na stercie (globalnie) posiadający wskaźnik do tego wskaźnika na stosie będzie się odwoływał szybciej, w przeciwieństwie, gdyby oba wskaźniki były utworzone na stercie? Chodzi o coś w stylu:
void f() {
K* wskaznik; 
g_obiekt.set_ptr(&wskaznik); /*g_obiekt to obiekt globalny lub pole innego obiektu - wewnątrz którego jest wskaźnik na ten wskaźnik utworzony w metodzie f()*/
//...
}
  1. Wydaje mi się, że tak, ale dla pewności zapytam: czy można w ogóle bezpiecznie utworzyć wskaźnik na stosie (w funkcji) do obiektu na stercie i na nim operować?

Z góry dziękuję za odpowiedzi.

1
  1. Nie, wyjątkiem są zmienne static
  2. Nie, zwykle to jest data segment.
  3. Tak
  4. Nie bardzo rozumiem pytanie, ale: nie
  5. Jasne że można bo i czemu niby nie? Co to znaczy "bezpiecznie"?
7

W C++ nie ma żadnych stosów i stert. To kwestia implementacji.

W C++ jest storage duration. To jest właściwość obiektu. Storage duration może być:

  • static (statyczne)
  • thread (związane z wątkiem)
  • automatic (automatyczne, to jest kojarzone ze stosem)
  • dynamic (dynamiczne, to jest kojarzone ze stertą)

Każdy obiekt, który ma zakres bloku (upraszczając: jest po prostu w funkcji) i nie ma przy nim słów kluczowych static albo extern ma automatic storage duration. To jest to, co popularne nazywa się stosem.

Każdy obiekt utworzony za pomocą wyrażenia z operatorem new ma dynamic storage duration. To jest to, co popularnie nazywa się stertą. Operator new nie zwraca tego obiektu, a jedynie jego adres. Tzn:

int *v = nullptr // obiekt v (wskaźnik do int), który ma automatic storage duration
v = new int{}; // new tworzy obiekt, który ma dynamic storage duration i zapisuje jego adres do obiektu v, który nadal jest automatic

Każdy obiekt, który nie ma dynamic storage duration ani thread storage duration oraz nie jest lokalny dla bloku (upraszczając: nie jest wewnątrz funkcji) ma static storage duration. Dodatkowo można nadać obiektowi static storage duration za pomocą słowa kluczowego static.

Zgodnie z powyższym:

  1. Każda taka zmienna będzie miała automatic storage duration, chyba, że będzie przy niej static albo extern.
  2. Nie. Zmienne globalne mają static storage duration, utworzone za pomocą operatora new - dynamic storage duration.
  3. Tak. To jaki storage duration ma obiekt wskazywany przez this nie ma żadnego znaczenia.
  4. Nic z tego nie rozumiem, ale jak piszesz "czy szybciej" to odpowiedam: użyj profilera i zobacz.
  5. To jest konieczność ponieważ new zwraca wskaźnik i trzeba go gdzieś zapisać.
0

Dzięki @Shalom
W tym 4 pytaniu chodzi o taką sytuację:
Mam dużą tablicę (vector) utworzony globalnie - czyli na stercie.
Mam również obiekt (powiedzmy, że utworzony również globalnie), który ma wskaźnik na dany element tej tablicy.
W funkcji natomiast bardzo często operuję na tym wskaźniku tj. np. iteruję po części tego wektora (odnoszę się do elementów tego vectora).
No i rozważam żeby zamiast robić w tym obiekcie wskaźnik na dany element tego vectora nie zrobić w funkcji takiego wskaźnika (który byłby na stosie) a w obiekcie zrobić wskaźnik na wskaźnik.
No i zastanawiam się, czy teoretycznie to powinno przyśpieszyć działanie programu?

0

<quote="1164036">4. Czy tworząc wskaźnik na stosie (w funkcji) do obiektu utworzonego na stercie (globalnie) inny obiekt utworzony na stercie (globalnie) posiadający wskaźnik do tego wskaźnika na stosie będzie się odwoływał szybciej, w przeciwieństwie, gdyby oba wskaźniki były utworzone na stercie? Chodzi o coś w stylu:

void f() {
K* wskaznik; 
g_obiekt.set_ptr(&wskaznik); /*g_obiekt to obiekt globalny lub pole innego obiektu - wewnątrz którego jest wskaźnik na ten wskaźnik utworzony w metodzie f()*/
//...
}

Wskaźnik to wskaźnik - efektywność jest taka sama, niezależnie od położenia w ram.

Kofcio napisał(a):
  1. Wydaje mi się, że tak, ale dla pewności zapytam: czy można w ogóle bezpiecznie utworzyć wskaźnik na stosie (w funkcji) do obiektu na stercie i na nim operować?

Tak.

Najwyraźniej masz poważne tyły natury ogólnej... jak każdy amator, wirtualny programista, wychowany na visualach.

0

Czy tworząc wskaźnik na stosie (w funkcji) do obiektu utworzonego na stercie (globalnie) inny obiekt utworzony na stercie (globalnie) posiadający wskaźnik do tego wskaźnika na stosie będzie się odwoływał szybciej, w przeciwieństwie, gdyby oba wskaźniki były utworzone na stercie?

Masło maślane.... Wskaźnik jako zmienna - może leżeć na stosie, ale można za pomocą jego alokować miejsce na stercie.

Wskaźnik to wskaźnik - efektywność jest taka sama, niezależnie od położenia w ram.

Umieszczanie zmiennych na stosie jest szybsze niż dynamiczna alokacja na stercie.

0

Nie wiem na czym jesteś "wychowany", ale więkość twoich technicznych wypowiedzi to brednie. - spartanPAGE

Ależ ja nie jestem wychowany na czymkolwiek...
no, po prostu: 40 lat praktyki robi swoje... :)

1
Kofcio napisał(a):
  1. Wydaje mi się, że tak, ale dla pewności zapytam: czy można w ogóle bezpiecznie utworzyć wskaźnik na stosie (w funkcji) do obiektu na stercie i na nim operować?

Można - o ile ten wskaźnik nie jest dalej nigdzie przekazywany (przez return, parametr wyjściowy lub pole w obiekcie).
Wydajnościowo bez sensu, ale różne są wymagania czasami.

Z drugiej strony w celach optymalizacji, robi się czasami coś odwrotnego - do lokalnego wskaźnika (na stercie) zapisuje wskaźnik do obiektu z atrybutu klasy lub zmiennej globalnej.
Może być szybsze (warto zajrzeć do ASM i/lub sprofilować).

0

Programista z 40 letnim doświadczeniem praktyki byłby rozważniejszy w dobieraniu słów, i miałby już dawno nabite z kilkaset postów na tym forum, a nie, że pisze jako guest. - Dawid90dd dzisiaj, 02:13

Ależ jestem tu logowany, tyle że się nie loguję... tak od około 10 lat.

Pamiętam nawet czasy gdy, np. Azerian, był tu... szczeniaczkiem. :)

0
vpiotr napisał(a):

Z drugiej strony w celach optymalizacji, robi się czasami coś odwrotnego - do lokalnego wskaźnika (na stercie) zapisuje wskaźnik do obiektu z atrybutu klasy lub zmiennej globalnej.
Może być szybsze (warto zajrzeć do ASM i/lub sprofilować).

@Vpiotr czy mógłbym Cię prosić o rozwinięcie tego, bo trochę tego nie rozumiem, a nie ukrywam, że moje dociekania wynikają właśnie z chęci optymalizacji.

vpiotr napisał(a):

do lokalnego wskaźnika (na stercie)

Czy "lokalny wskaźnik" nie powinien być na stosie?
Chodzi Ci o utworzenie wskaźnika globalnie (na stercie) czy lokalnie wewnątrz funkcji (na stosie)?

vpiotr napisał(a):

zapisuje wskaźnik do obiektu z atrybutu klasy lub zmiennej globalnej.

Czyli wskaźnik na wskaźnik?

Czy mógłbyś podać jakiś prosty przykład?
Z góry dziękuję za pomoc!

1

Sorry, oczywiscie chodzilo mi o lokalny wskaznik na stosie. Przyklad optymalizacji pisany z glowy z komorki na kolanie:

void myclass::foo () {
  int *lptr = this-> currTab;
  while (cos tam) {
   // zrob cos z lptr zamiast z currTab
  }
}
1

Jeżeli gadamy o optymalizacji to:

  1. Alokacja czegokolwiek na stosie będzie szybsza niż na stercie; na stosie kompilator może alokować zmienne hurtowo - zwykle nie ma znaczenia ile zmiennych lokalnych jest w funkcji bo alokacja to tylko przesunięcie wierzchołka stosu, dealokacja podobnie. W przypadku pamięci dynamicznej alokacja jest znacznei bardziej złożonym procesem i może kosztować kilkadziesiąt-kilkaset cykli CPU.

  2. Czas dostępu do danych wskazywanych przez wskaźnik jest zależny od znacznie większej liczby czynników niż tylko czy obiekt jest na stosie, czy stercie. Tu główną rolę odgrywa użycie danej pamięci i sposób dostępu. Może mieć też znaczenie, który rdzeń procesora chce się odwołać do danej pamięci. Wierzchołek stosu jest jednak zwykle "gorący" jeśli chodzi o cache, więc mając wskaźnik na obiekt na stosie mamy dość duże prawdopodobieństwo, że ten obiekt jest w cache procesora, a zatem dostęp będzie szybki. W przypadku skoku w losowe miejsce sterty, możemy mieć albo szczęście (jeśli np. niedawno ten obszar pamięci był już używany i nie wyleciał z cache), albo małego pecha (obiekt jest w cache, ale tylko np. L2/L3, bo był odczytywany przez inny rdzeń procesora) albo większego pecha (cache-miss i paręset cykli w plecy) albo ekstremalnego pecha (strona pamięci została wyrzucona z RAMu na dysk...). CPU potrafią też robić różne sztuczki aby zredukować nieco ból związany z pudłowaniem w cache i np. jeśli skanujesz pamięć sekwencyjnie to zrobią odpowiedni prefetching; ale jeśli skaczesz po pamięci losowo (np. bo masz dużo wskaźników), to spodziewaj się drastycznego spowolnienia. Ułożenie danych względem linii cache podobnie może mieć duży wpływ - np. jeśli obiekt, do którego sięgasz wskaźnikiem zajmuje dwie linie, mimo że zmieściłby się na jednej, to też zapłacisz podwójnie za dostęp. Ale i na odwrót - więcej zapłacisz za zapis, jeśli z dwóch wątków będziesz próbował dobierać się do różnych obiektów pechowo leżących na tej samej linii cache.

0

Dzięki wszystkim za pomoc.
Może na koniec jeszcze przedstawię jak to u mnie wygląda i do czego ja zmierzałem.

  1. Celem aplikacji, którą piszę jest analiza pewnych tych danych i pisanie modeli matematycznych pod te dane.
  2. Mam więc vector z danymi umieszczony globalnie - tak, by każdy z wątków mógł się do nich odnosić. Danych jest też bardzo dużo (około 1GB) więc nie zmieszczą się na stosie.
  3. W metodzie, która analizuje te dane tworzę wskaźnik na element vectora, który przekazuję jeszcze do pewnych obiektów, które muszą odnosić się do wskazywanych aktualnie danych.
  4. następnie w tej metodzie uruchamiam pętlę i za każdą iteracją zmieniam wskaźnik o 1 (analizuję wszystkie dane).

W uproszczonej wersji wygląda to mniej więcej tak:

vector<Dane> dane; //zmienna globalna

class Analizator
	{
	public:
		ObiektPomocniczy obiekt_pomocniczy;
		void analizuj()
			{
			Dane* wskaznik = &dane[0];
			Dane* end = &dane[dane.size()];
			obiekt_pomocniczy.SetPtr(wskaznik);
			//...

			//wykonaj operacje na pierwszym elemencie
			while(++wskaznik < end)
				{
				//wykonuje operacje na pozostałych elementach
				}

			}
	};

Jeśli macie jakieś uwagi co do tego kodu to dajcie znać.
Jeszcze raz dzięki.

0

Pierwszy element to ty akurat właśnie zgubiłeś... :)

0

@Kofcio: nie wiem co kombinujesz, ale zmienne globalne to gwarancja kłopotów (prędzej czy później).
Coś strasznie gmatwasz. Jesteś pewien że takich struktur nie będziesz potrzebował > 1 w programie?

0

Wskaźnik to wskaźnik - efektywność jest taka sama, niezależnie od położenia w ram.
Sęk w tym, że czas dostępu do stosu jest inny niż czas dostępu do sterty. Generalnie operacje I/O na stercie są wolniejsze.
bzdury chyba

0
pingwindyktator napisał(a):

Wskaźnik to wskaźnik - efektywność jest taka sama, niezależnie od położenia w ram.
Sęk w tym, że czas dostępu do stosu jest inny niż czas dostępu do sterty. Generalnie operacje I/O na stercie są wolniejsze.

Brednie...

Stos jest używany cały czas, zatem ta część ramu, tz. z wierzchołka stosu, będzie/jest praktycznie zawsze w cache.
Innych różnic tu nie ma.

1

I to wpływa na różnice wydajności? - pingwindyktator dzisiaj, 21:17

Nie specjalnie, ponieważ dane czytane/używane - z dowolnego obszaru ram,
są i tak są ładowane do cache, przy pierwszym dostępie...

Zatem przy cacheu o rozmiarach już 1MB masz w zasadzie cały program w tym cache,
no bo nie ma kodu algorytmu tak zajebistych rozmiarów... pomijając być może produkty Adobe, MS, itp. szmaciarzy, rzecz jasna. :)

0
fju napisał(a):

I to wpływa na różnice wydajności? - pingwindyktator dzisiaj, 21:17

Nie specjalnie, ponieważ dane czytane/używane - z dowolnego obszaru ram,
są i tak są ładowane do cache, przy pierwszym dostępie...

Problem w tym, że cache jest mały i dane są z niego również wyrzucane. Ponadto cache nie trzyma pojedynczych bajtów, a niepodzielne paczki po np. 32 bajty. Dane na stosie z natury są upakowane w obszarze ciągłym pamięci, dosyć gęsto, co daje dużą efektywność wykorzystania cache. Natomiast sterta jest pofragmentowana i np. dwie kolejne alokacje dynamiczne mogą zwrócić wskaźniki do odległych obszarów. Kluczem do uzyskania dobrej wydajności nie jest stos vs. sterta, a takie zaprojektowanie struktur danych, aby było jak najmniej wskaźników, jak najpełniejsze wykorzystanie linii cache, a dostęp do pamięci możliwie łatwy do przewidzenia przez procesor (np. sekwencyjny). Wektory i tablice są pod tym względem właśnie dobre. Małe obiekty porozrzucane po całej starcie i powiązane wskaźnikami są bardzo złe.

Zatem przy cacheu o rozmiarach już 1MB masz w zasadzie cały program w tym cache,
no bo nie ma kodu algorytmu tak zajebistych rozmiarów... pomijając być może produkty Adobe, MS, itp. szmaciarzy, rzecz jasna. :)

Praktycznie żaden kompilator sensownego języka nie zmieści się w 1MB. Stdlib c++ też nie, tym bardziej boost czy Qt. Poza tym cache L2, L3 i L4 jest współdzielony pomiędzy kod i dane oraz część się marnuje ze względu na fragmentację. Dedykowanego, szybkiego cache dla kodu jest zwykle kilkadziesiąt kB, a w procesorach klasy Xeon może niskie kilkaset kB.

Zarejestruj się i dołącz do największej społeczności programistów w Polsce.

Otrzymaj wsparcie, dziel się wiedzą i rozwijaj swoje umiejętności z najlepszymi.