Co tak naprawdę oznacza „Pamięć przydzielona w czasie kompilacji”?


159

W językach programowania, takich jak C i C ++, ludzie często odnoszą się do statycznej i dynamicznej alokacji pamięci. Rozumiem tę koncepcję, ale fraza „Cała pamięć została przydzielona (zarezerwowana) w czasie kompilacji” zawsze mnie dezorientuje.

Kompilacja, jak rozumiem, konwertuje kod C / C ++ wysokiego poziomu na język maszynowy i generuje plik wykonywalny. W jaki sposób przydzielana jest pamięć w skompilowanym pliku? Czy pamięć nie jest zawsze przydzielana w pamięci RAM ze wszystkimi elementami zarządzania pamięcią wirtualną?

Czy alokacja pamięci nie jest z definicji pojęciem środowiska uruchomieniowego?

Jeśli w moim kodzie C / C ++ utworzę statycznie przydzieloną zmienną 1 KB, czy spowoduje to zwiększenie rozmiaru pliku wykonywalnego o tę samą wartość?

To jest jedna ze stron, na której fraza jest używana pod nagłówkiem „Alokacja statyczna”.

Powrót do podstaw: alokacja pamięci, spacer po historii


kod i dane są całkowicie oddzielone w większości nowoczesnych architektur. podczas gdy pliki źródłowe zawierają oba dane kodu w tym samym miejscu, kosz zawiera tylko odniesienia do danych. Oznacza to, że dane statyczne w źródle są rozpoznawane tylko jako odniesienia.
Cholthi Paul Ttiopic

Odpowiedzi:


184

Pamięć przydzielona w czasie kompilacji oznacza, że ​​kompilator rozwiązuje problem w czasie kompilacji, w którym pewne rzeczy zostaną przydzielone w mapie pamięci procesu.

Na przykład rozważmy tablicę globalną:

int array[100];

Kompilator wie w czasie kompilacji rozmiar tablicy i rozmiar an int, więc zna cały rozmiar tablicy w czasie kompilacji. Również zmienna globalna ma domyślnie statyczny czas trwania: jest alokowana w obszarze pamięci statycznej obszaru pamięci procesu (sekcja .data / .bss). Biorąc pod uwagę te informacje, kompilator decyduje podczas kompilacji, w jakim adresie tego statycznego obszaru pamięci będzie tablica .

Oczywiście te adresy pamięci są adresami wirtualnymi. Program zakłada, że ​​ma własną całą przestrzeń pamięci (na przykład od 0x00000000 do 0xFFFFFFFF). Dlatego kompilator mógł wykonać założenia typu „OK, tablica będzie miała adres 0x00A33211”. W czasie wykonywania te adresy są tłumaczone na adresy rzeczywiste / sprzętowe przez MMU i system operacyjny.

Wartość zainicjowanej wartości statycznej pamięci masowej jest nieco inna. Na przykład:

int array[] = { 1 , 2 , 3 , 4 };

W naszym pierwszym przykładzie kompilator zdecydował tylko, gdzie tablica zostanie przydzielona, ​​przechowując te informacje w pliku wykonywalnym.
W przypadku rzeczy zainicjowanych wartością, kompilator również wstrzykuje wartość początkową tablicy do pliku wykonywalnego i dodaje kod, który mówi programowi ładującemu, że po przydzieleniu tablicy przy uruchomieniu programu tablica powinna zostać wypełniona tymi wartościami.

Oto dwa przykłady zestawu wygenerowanego przez kompilator (GCC4.8.1 z celem x86):

Kod C ++:

int a[4];
int b[] = { 1 , 2 , 3 , 4 };

int main()
{}

Zespół wyjściowy:

a:
    .zero   16
b:
    .long   1
    .long   2
    .long   3
    .long   4
main:
    pushq   %rbp
    movq    %rsp, %rbp
    movl    $0, %eax
    popq    %rbp
    ret

Jak widać, wartości są wprowadzane bezpośrednio do zespołu. W tablicy akompilator generuje zerową inicjalizację 16 bajtów, ponieważ Standard mówi, że statyczne przechowywane rzeczy powinny być domyślnie inicjowane do zera:

8.5.9 (Inicjatory) [Uwaga]:
Każdy obiekt o statycznym czasie trwania przechowywania jest inicjowany do zera podczas uruchamiania programu przed jakąkolwiek inną inicjalizacją. W niektórych przypadkach dodatkowa inicjalizacja jest wykonywana później.

Zawsze sugeruję ludziom rozmontowanie kodu, aby zobaczyć, co kompilator naprawdę robi z kodem C ++. Dotyczy to klas pamięci / czasu trwania (jak to pytanie) do zaawansowanych optymalizacji kompilatora. Możesz poinstruować kompilator, aby wygenerował asemblację, ale w Internecie są wspaniałe narzędzia do tego w przyjazny sposób. Moim ulubionym jest GCC Explorer .


2
Dzięki. To dużo wyjaśnia. Kompilator wyświetla więc coś równoważnego „zarezerwuj pamięć od 0xABC do 0xXYZ dla tablicy zmiennych [] itd.” a program ładujący używa tego do rzeczywistego przydzielenia go tuż przed uruchomieniem programu?
Talha powiedział

1
@TalhaSayed dokładnie. Zobacz edycję, aby spojrzeć na przykład
Manu343726

2
@Secko Uprościłem sprawy. To tylko wzmianka o tym, że program działa poprzez pamięć wirtualną, ale skoro pytanie nie dotyczy pamięci wirtualnej, nie rozszerzyłem tematu. Wskazałem tylko, że kompilator może robić założenia dotyczące adresów pamięci w czasie kompilacji, dzięki pamięci wirtualnej.
Manu343726

2
@Secko yes. Myślę, że "wygenerowane" to lepsze określenie.
Manu343726,

2
„Jest przydzielony w statycznym obszarze pamięci procesu” Czytanie, które przydzieliło pewne statyczne obszary sutkowe w mojej przestrzeni pamięci procesu.
Radiodef

27

Pamięć przydzielona w czasie kompilacji oznacza po prostu, że nie będzie dalszej alokacji w czasie wykonywania - brak wywołań do malloc, new lub innych metod alokacji dynamicznej. Będziesz mieć stałą ilość użycia pamięci, nawet jeśli nie potrzebujesz całej tej pamięci przez cały czas.

Czy alokacja pamięci nie jest z definicji pojęciem środowiska uruchomieniowego?

Pamięć nie jest używana przed uruchomieniem, ale bezpośrednio przed uruchomieniem jej alokacja jest obsługiwana przez system.

Jeśli w moim kodzie C / C ++ utworzę statycznie przydzieloną zmienną 1 KB, czy spowoduje to zwiększenie rozmiaru pliku wykonywalnego o tę samą wartość?

Samo zadeklarowanie wartości statycznej nie zwiększy rozmiaru pliku wykonywalnego o więcej niż kilka bajtów. Zadeklarowanie wartości początkowej niezerowej spowoduje (w celu utrzymania tej wartości początkowej). Zamiast tego konsolidator po prostu dodaje tę kwotę 1 KB do wymagań dotyczących pamięci, które program ładujący systemu tworzy dla Ciebie bezpośrednio przed wykonaniem.


1
jeśli napiszę static int i[4] = {2 , 3 , 5 ,5 }, zwiększy się o rozmiar pliku wykonywalnego o 16 bajtów. Powiedziałeś: „Po prostu zadeklarowanie wartości statycznej nie zwiększy rozmiaru pliku wykonywalnego o więcej niż kilka bajtów. Zadeklarowanie go z wartością początkową niezerową spowoduje”. Zadeklarowanie wartości początkowej spowoduje, co to oznacza.
Suraj Jain

Twój plik wykonywalny ma dwa obszary dla danych statycznych - jeden dla niezainicjowanych statystyk i jeden dla zainicjowanych statystyk. Niezainicjalizowany obszar to tak naprawdę tylko wskazanie rozmiaru; kiedy program jest uruchamiany, ten rozmiar jest używany do powiększania obszaru pamięci statycznej, ale sam program nie musiał przechowywać niczego więcej niż ilość używanych niezainicjowanych danych. Aby uzyskać zainicjowaną statystykę, twój program musi przechowywać nie tylko rozmiar (każdego) statycznego, ale także to, do czego jest inicjowany. Zatem w twoim przykładzie twój program będzie zawierał 2, 3, 5 i 5.
mah

Jest to implementacja zdefiniowana jako miejsce, w którym jest umieszczana / jak jest przydzielana, ale nie jestem pewien, czy rozumiem potrzebę wiedzy.
mah

23

Pamięć przydzielona w czasie kompilacji oznacza, że ​​po załadowaniu programu pewna część pamięci zostanie natychmiast przydzielona, ​​a rozmiar i (względna) pozycja tej alokacji jest określana w czasie kompilacji.

char a[32];
char b;
char c;

Te 3 zmienne są „przydzielane w czasie kompilacji”, co oznacza, że ​​kompilator oblicza ich rozmiar (który jest ustalony) w czasie kompilacji. Zmienna abędzie przesunięciem w pamięci, powiedzmy, wskazującym na adres 0, bbędzie wskazywać na adres 33 i c34 (zakładając, że nie ma optymalizacji wyrównania). Zatem przydzielenie 1Kb danych statycznych nie zwiększy rozmiaru twojego kodu , ponieważ zmieni tylko przesunięcie w nim. Rzeczywista przestrzeń zostanie przydzielona w czasie ładowania .

Prawdziwa alokacja pamięci zawsze odbywa się w czasie wykonywania, ponieważ jądro musi to śledzić i aktualizować swoje wewnętrzne struktury danych (ile pamięci jest przydzielane dla każdego procesu, stron i tak dalej). Różnica polega na tym, że kompilator już zna rozmiar wszystkich danych, których zamierzasz użyć, i jest on przydzielany zaraz po wykonaniu programu.

Pamiętaj też, że mówimy o adresach względnych . Rzeczywisty adres, pod którym będzie się znajdować zmienna, będzie inny. W czasie ładowania jądro zarezerwuje trochę pamięci dla procesu, powiedzmy pod adresem x, a wszystkie zakodowane na stałe adresy zawarte w pliku wykonywalnym zostaną zwiększone o xbajty, tak że zmienna aw przykładzie będzie pod adresem x, b pod adresem x+33i wkrótce.


17

Dodanie zmiennych na stosie, które zajmują N bajtów, nie (koniecznie) nie zwiększa rozmiaru pojemnika o N bajtów. W rzeczywistości przez większość czasu będzie dodawać tylko kilka bajtów.
Zacznijmy od przykładu jak dodanie 1000 znaków w kodzie woli zwiększyć rozmiar kosza w sposób liniowy.

Jeśli 1k jest ciągiem tysiąca znaków, to jest tak zadeklarowane

const char *c_string = "Here goes a thousand chars...999";//implicit \0 at end

i wtedy vim your_compiled_binmiałbyś, faktycznie byłbyś w stanie zobaczyć ten ciąg gdzieś w koszu. W takim przypadku tak: plik wykonywalny będzie większy o 1 k, ponieważ zawiera cały ciąg.
Jeśli jednak przydzielisz tablicę ints, chars lublong s na stosie i przypiszesz ją w pętli, coś wzdłuż tych linii

int big_arr[1000];
for (int i=0;i<1000;++i) big_arr[i] = some_computation_func(i);

wtedy nie: nie zwiększy kosza ... przez 1000*sizeof(int)
Alokację w czasie kompilacji oznacza to, co teraz zrozumiałeś to oznacza (na podstawie twoich komentarzy): skompilowany kosz zawiera informacje, których system potrzebuje, aby wiedzieć, ile pamięci jaka funkcja / blok będzie potrzebna, gdy zostanie wykonana, wraz z informacją o rozmiarze stosu, którego wymaga Twoja aplikacja. To jest to, co system przydzieli, kiedy wykona twój bin, a twój program stanie się procesem (cóż, wykonanie twojego bin to proces, który ... cóż, rozumiesz, o czym mówię).
Oczywiście nie maluję tutaj całego obrazu: Kosz zawiera informacje o tym, jak duży stos będzie faktycznie potrzebny. Na podstawie tych informacji (między innymi), system zarezerwuje fragment pamięci, zwany stosem, nad którym program będzie mógł swobodnie rządzić. Pamięć stosu nadal jest przydzielana przez system, gdy proces (wynik wykonania binarki) jest inicjowany. Następnie proces zarządza pamięcią stosu za Ciebie. Kiedy funkcja lub pętla (dowolny typ bloku) jest wywoływana / wykonywana, zmienne lokalne dla tego bloku są umieszczane na stosie i są usuwane (pamięć stosu jest „zwalniana”, że tak powiem) do wykorzystania przez inne funkcje / bloki. Tak deklarujęint some_array[100]doda tylko kilka bajtów dodatkowych informacji do kosza, co powie systemowi, że funkcja X będzie wymagać100*sizeof(int) + dodatkowa przestrzeń księgowa.


Wielkie dzięki. Jeszcze jedno pytanie, czy zmienne lokalne dla funkcji również są przydzielane w ten sam sposób podczas kompilacji?
Talha powiedział

@TalhaSayed: Tak, właśnie to miałem na myśli, kiedy powiedziałem: „informacje, których system potrzebuje, aby wiedzieć, ile pamięci będzie wymagać funkcja / blok”. W momencie wywołania funkcji system przydzieli wymaganą pamięć dla tej funkcji. W momencie, gdy funkcja powróci, pamięć ta zostanie ponownie zwolniona.
Elias Van Ootegem

A jeśli chodzi o komentarze w kodzie C: to niekoniecznie się faktycznie dzieje. Na przykład łańcuch najprawdopodobniej zostanie przydzielony tylko raz, w czasie kompilacji. Dlatego nigdy nie jest „zwalniany” (myślę również, że terminologia jest zwykle używana tylko wtedy, gdy przydzielasz coś dynamicznie), inie jest „zwalniana” ani też. Gdyby irezydował w pamięci, po prostu zostałby zepchnięty na stos, coś, co nie jest uwolnione w tym znaczeniu tego słowa, pomijając to ilub cbędzie przechowywane w rejestrach przez cały czas. Oczywiście wszystko zależy od kompilatora, co oznacza, że ​​nie jest tak czarno-biały.
phant0m

@ phant0m: Nigdy nie powiedziałem, że łańcuch jest alokowany na stosie, tylko wskaźnik też byłby, sam ciąg znajdowałby się w pamięci tylko do odczytu. Wiem, że pamięć związana ze zmiennymi lokalnymi nie jest zwalniana w sensie free()wywołań, ale pamięć stosu, której używali, jest wolna do wykorzystania przez inne funkcje, gdy funkcja, którą wymieniłem, powróci.
Usunąłem

O, rozumiem. W takim razie weź mój komentarz tak, aby oznaczał: „Byłem zdezorientowany Twoim sformułowaniem”.
phant0m

16

Na wielu platformach wszystkie globalne lub statyczne alokacje w każdym module zostaną skonsolidowane przez kompilator w trzy lub mniej skonsolidowanych przydziałów (jeden dla niezainicjowanych danych (często nazywany „bss”), drugi dla zainicjowanych danych do zapisu (często nazywanych „danymi” ) i jeden dla stałych danych („const”)), a wszystkie globalne lub statyczne alokacje każdego typu w programie zostaną skonsolidowane przez linker w jeden globalny dla każdego typu. Na przykład, zakładając intcztery bajty, moduł ma następujące statyczne alokacje:

int a;
const int b[6] = {1,2,3,4,5,6};
char c[200];
const int d = 23;
int e[4] = {1,2,3,4};
int f;

powiedziałby linkerowi, że potrzebuje 208 bajtów na bss, 16 bajtów na "dane" i 28 bajtów na "const". Co więcej, każde odniesienie do zmiennej zostanie zastąpione selektorem obszaru i przesunięciem, więc a, b, c, d i e zostaną zastąpione przez bss + 0, const + 0, bss + 4, const + 24, data +0 lub bss + 204, odpowiednio.

Kiedy program jest połączony, wszystkie obszary bss ze wszystkich modułów są łączone razem; podobnie jak obszary danych i const. Dla każdego modułu adres wszelkich zmiennych odnoszących się do bss zostanie zwiększony o rozmiar obszarów bss wszystkich poprzedzających modułów (ponownie, podobnie z danymi i const). Tak więc, kiedy linker zostanie ukończony, każdy program będzie miał jedną alokację bss, jedną alokację danych i jedną alokację const.

Kiedy program jest ładowany, w zależności od platformy zwykle dzieje się jedna z czterech rzeczy:

  1. Plik wykonywalny wskaże, ile bajtów potrzebuje dla każdego rodzaju danych oraz - dla zainicjowanego obszaru danych, w którym można znaleźć początkową zawartość. Będzie również zawierać listę wszystkich instrukcji, które używają adresu względnego bss, data lub const. System operacyjny lub program ładujący przydzieli odpowiednią ilość miejsca dla każdego obszaru, a następnie doda adres początkowy tego obszaru do każdej instrukcji, która tego potrzebuje.

  2. System operacyjny przydzieli porcję pamięci do przechowywania wszystkich trzech rodzajów danych i poda aplikacji wskaźnik do tej porcji pamięci. Każdy kod, który używa danych statycznych lub globalnych, wyłuskuje je względem tego wskaźnika (w wielu przypadkach wskaźnik będzie przechowywany w rejestrze przez cały okres istnienia aplikacji).

  3. System operacyjny początkowo nie przydzieli żadnej pamięci do aplikacji, z wyjątkiem tego, co przechowuje jej kod binarny, ale pierwszą rzeczą, którą aplikacja zrobi, będzie zażądanie odpowiedniego przydziału z systemu operacyjnego, który na zawsze będzie przechowywać w rejestrze.

  4. System operacyjny początkowo nie przydzieli miejsca dla aplikacji, ale aplikacja zażąda odpowiedniego przydziału przy uruchomieniu (jak wyżej). Aplikacja będzie zawierała listę instrukcji z adresami, które należy zaktualizować, aby odzwierciedlić miejsce przydzielenia pamięci (jak w przypadku pierwszego stylu), ale zamiast łatania aplikacji przez moduł ładujący system operacyjny, aplikacja będzie zawierała wystarczającą ilość kodu, aby załatać samą siebie .

Wszystkie cztery podejścia mają zalety i wady. Jednak w każdym przypadku kompilator skonsoliduje dowolną liczbę zmiennych statycznych w ustaloną niewielką liczbę żądań pamięci, a konsolidator skonsoliduje je wszystkie w niewielką liczbę skonsolidowanych alokacji. Mimo że aplikacja będzie musiała otrzymać porcję pamięci z systemu operacyjnego lub programu ładującego, to kompilator i konsolidator są odpowiedzialne za przydzielanie poszczególnych elementów z tej dużej porcji do wszystkich indywidualnych zmiennych, które jej potrzebują.


13

Rdzeń twojego pytania jest następujący: „W jaki sposób pamięć jest„ alokowana ”w skompilowanym pliku? Czy pamięć nie jest zawsze przydzielana w pamięci RAM wraz z całym zarządzaniem pamięcią wirtualną? Czy alokacja pamięci nie jest z definicji pojęciem środowiska wykonawczego?”

Myślę, że problem polega na tym, że istnieją dwie różne koncepcje związane z alokacją pamięci. Uogólniając, alokacja pamięci jest procesem, w którym mówimy „ta pozycja danych jest przechowywana w tej konkretnej części pamięci”. W nowoczesnym systemie komputerowym obejmuje to dwuetapowy proces:

  • W niektórych systemach decyduje się, pod jakim adresem wirtualnym będzie przechowywany przedmiot
  • Adres wirtualny jest mapowany na adres fizyczny

Ten ostatni proces jest czysto wykonywany w czasie wykonywania, ale pierwszy można wykonać w czasie kompilacji, jeśli dane mają znany rozmiar i wymagana jest ich stała liczba. Oto w zasadzie, jak to działa:

  • Kompilator widzi plik źródłowy zawierający linię, która wygląda trochę tak:

    int c;
  • Tworzy dane wyjściowe dla asemblera, który instruuje go, aby zarezerwował pamięć dla zmiennej „c”. Może to wyglądać tak:

    global _c
    section .bss
    _c: resb 4
  • Kiedy asembler działa, utrzymuje licznik, który śledzi przesunięcia każdego elementu od początku „segmentu” pamięci (lub „sekcji”). Jest to podobne do części bardzo dużej „struktury”, która zawiera wszystko w całym pliku, ale nie ma obecnie przydzielonej żadnej pamięci i może znajdować się gdziekolwiek. Notuje w tabeli, która _cma określone przesunięcie (powiedzmy 510 bajtów od początku segmentu), a następnie zwiększa swój licznik o 4, więc następna taka zmienna będzie miała (np.) 514 bajtów. Dla każdego kodu, który potrzebuje adresu _c, po prostu umieszcza 510 w pliku wyjściowym i dodaje uwagę, że wyjście wymaga adresu segmentu, który zawiera _cdodanie do niego później.

  • Konsolidator pobiera wszystkie pliki wyjściowe asemblera i bada je. Określa adres dla każdego segmentu, tak aby się nie nakładały, i dodaje niezbędne przesunięcia, aby instrukcje nadal odnosiły się do właściwych elementów danych. W przypadku niezainicjowanej pamięci, takiej jak ta zajmowana przezc(asemblerowi powiedziano, że pamięć będzie niezainicjowana przez fakt, że kompilator umieścił ją w segmencie '.bss', który jest nazwą zarezerwowaną dla niezainicjowanej pamięci), zawiera pole nagłówka w swoim wyjściu, które informuje system operacyjny ile trzeba zarezerwować. Może zostać przeniesiony (i zwykle jest), ale zwykle jest zaprojektowany tak, aby był ładowany bardziej efektywnie pod jednym określonym adresem pamięci, a system operacyjny będzie próbował załadować go pod tym adresem. W tym momencie mamy całkiem niezłe pojęcie, przez jaki adres wirtualny będzie używany c.

  • Adres fizyczny nie zostanie w rzeczywistości określony, dopóki program nie zostanie uruchomiony. Jednak z punktu widzenia programisty adres fizyczny jest w rzeczywistości nieistotny - nigdy się nawet nie dowiemy, co to jest, ponieważ system operacyjny zwykle nie zawraca sobie głowy informowaniem nikogo, może się często zmieniać (nawet gdy program jest uruchomiony), a i tak głównym celem systemu operacyjnego jest oderwanie tego.


9

Plik wykonywalny opisuje, jakie miejsce należy przydzielić na zmienne statyczne. Ta alokacja jest wykonywana przez system, kiedy uruchamiasz plik wykonywalny. Więc twoja statyczna zmienna 1kB nie zwiększy rozmiaru pliku wykonywalnego o 1kB:

static char[1024];

O ile oczywiście nie określisz inicjatora:

static char[1024] = { 1, 2, 3, 4, ... };

Tak więc, oprócz „języka maszynowego” (tj. Instrukcji procesora), plik wykonywalny zawiera opis wymaganego układu pamięci.


5

Pamięć można przydzielić na wiele sposobów:

  • w stercie aplikacji (cała sterta jest przydzielana do aplikacji przez system operacyjny podczas uruchamiania programu)
  • w stercie systemu operacyjnego (dzięki czemu można pobrać coraz więcej)
  • w sterowanym przez garbage collector stercie (tak samo jak oba powyżej)
  • na stosie (dzięki czemu możesz uzyskać przepełnienie stosu)
  • zarezerwowane w segmencie kodu / danych twojego pliku binarnego (wykonywalnego)
  • w zdalnym miejscu (plik, sieć - i otrzymujesz uchwyt, a nie wskaźnik do tej pamięci)

Teraz twoje pytanie brzmi, co to jest „pamięć przydzielana w czasie kompilacji”. Zdecydowanie jest to po prostu niepoprawnie sformułowane powiedzenie, które ma odnosić się do alokacji segmentów binarnych lub alokacji stosu, lub w niektórych przypadkach nawet do alokacji sterty, ale w tym przypadku alokacja jest ukryta przed oczami programisty przez niewidzialne wywołanie konstruktora. Lub prawdopodobnie osoba, która powiedziała, że ​​chce tylko powiedzieć, że pamięć nie jest przydzielana na stosie, ale nie wiedziała o alokacji stosu lub segmentu (lub nie chciała wdawać się w tego rodzaju szczegóły).

Ale w większości przypadków osoba chce tylko powiedzieć, że ilość przydzielonej pamięci jest znana w czasie kompilacji .

Rozmiar binarny zmieni się tylko wtedy, gdy pamięć zostanie zarezerwowana w kodzie lub segmencie danych aplikacji.


1
Ta odpowiedź jest myląca (lub niejasna), ponieważ mówi o „stertach aplikacji”, „stertach systemu operacyjnego” i „stertach GC”, tak jakby były to wszystkie znaczące pojęcia. Wnioskuję, że przez # 1 próbowałeś powiedzieć, że niektóre języki programowania mogą (hipotetycznie) używać schematu "alokacji sterty", który przydziela pamięć z bufora o stałej wielkości w sekcji .data, ale wydaje się to na tyle nierealne, aby było szkodliwe w rozumieniu PO. Ad # 2 i # 3, obecność GC tak naprawdę niczego nie zmienia. I w punkcie 5, pominęliście stosunkowo DUŻO ważniejsze rozróżnienie między .datai .bss.
Quuxplusone

4

Masz rację. Pamięć jest faktycznie przydzielana (stronicowana) w czasie ładowania, tj. Gdy plik wykonywalny jest wprowadzany do (wirtualnej) pamięci. W tym momencie można również zainicjować pamięć. Kompilator po prostu tworzy mapę pamięci. [Nawiasem mówiąc, przestrzenie stosu i sterty są również przydzielane podczas ładowania!]


2

Myślę, że musisz się trochę cofnąć. Pamięć przydzielona w czasie kompilacji… Co to może znaczyć? Czy może to oznaczać, że pamięć w chipach, które nie zostały jeszcze wyprodukowane, dla komputerów, które nie zostały jeszcze zaprojektowane, jest w jakiś sposób rezerwowana? Nie, podróże w czasie, żadnych kompilatorów, które mogą manipulować wszechświatem.

Musi więc oznaczać, że kompilator generuje instrukcje, aby w jakiś sposób przydzielić tę pamięć w czasie wykonywania. Ale jeśli spojrzysz na to pod odpowiednim kątem, kompilator generuje wszystkie instrukcje, więc jaka może być różnica. Różnica polega na tym, że decyduje kompilator, aw czasie wykonywania kod nie może zmieniać ani modyfikować swoich decyzji. Jeśli zdecydował, że potrzebuje 50 bajtów w czasie kompilacji, w czasie wykonywania, nie możesz zdecydować o przydzieleniu 60 - ta decyzja została już podjęta.


Lubię odpowiedzi, które używają metody Sokratejskiej, ale nadal przegłosowałem Cię za błędny wniosek, że „kompilator generuje instrukcje, aby jakoś przydzielić tę pamięć w czasie wykonywania”. Zapoznaj się z najczęściej ocenianą odpowiedzią, aby zobaczyć, jak kompilator może „przydzielić pamięć” bez generowania żadnych „instrukcji” w czasie wykonywania. (Należy pamiętać, że „instrukcje” w kontekście montażu języka ma znaczenie specyficzne, tzn wykonywalne rozkazy. Ty może być użycie słowa potocznie do czegoś w rodzaju „przepis”, ale w tym kontekście, że będzie tylko zmylić PO. )
Quuxplusone

1
@Quuxplusone: Przeczytałem (i przegłosowałem) tę odpowiedź. I nie, moja odpowiedź nie odnosi się konkretnie do problemu zainicjalizowanych zmiennych. Nie dotyczy również samomodyfikującego się kodu. Chociaż ta odpowiedź jest doskonała, nie dotyczyła tego, co uważam za ważną kwestię - umieszczania rzeczy w kontekście. Stąd moja odpowiedź, która, mam nadzieję, pomoże PO (i innym) zatrzymać się i pomyśleć o tym, co się dzieje lub może się dziać, kiedy mają problemy, których nie rozumieją.
jmoreno

@Quuxplusone: Przepraszam, jeśli stawiam tu fałszywe zarzuty, ale rozumiem, że byłeś jedną z osób, które również odpowiedziały mi. Jeśli tak, czy mógłbyś strasznie wskazać, która część mojej odpowiedzi była głównym powodem, dla którego to zrobiłem, i czy zechciałabyś również sprawdzić moją zmianę? Wiem, że pominięto kilka bitów o prawdziwej wewnętrznych, w jaki sposób pamięć stos jest zarządzany, więc teraz dodaje trochę o moim nie jest teraz w 100% z dokładnością do mojej odpowiedzi anyways :)
Elias Van Ootegem

@jmoreno Kwestia, o której wspomniałeś: „Czy może to oznaczać, że pamięć na chipach, które nie zostały jeszcze wyprodukowane, dla komputerów, które nie zostały jeszcze zaprojektowane, jest w jakiś sposób zarezerwowana? Nie.” jest dokładnie fałszywym znaczeniem, które sugeruje słowo „alokacja”, które od początku mnie zdezorientowało. Podoba mi się ta odpowiedź, ponieważ odnosi się dokładnie do problemu, który próbowałem wskazać. Żadna z odpowiedzi tutaj tak naprawdę nie dotyczyła tego konkretnego punktu. Dzięki.
Talha powiedział

2

Jeśli nauczysz się programowania w asemblerze, zobaczysz, że musisz wyrzeźbić segmenty dla danych, stosu i kodu, itp. Segment danych to miejsce, w którym żyją ciągi i liczby. Segment kodu to miejsce, w którym żyje Twój kod. Segmenty te są wbudowane w program wykonywalny. Oczywiście rozmiar stosu jest również ważny ... nie chciałbyś, aby stos się przepełnił !

Więc jeśli twój segment danych ma 500 bajtów, twój program ma 500 bajtów. Jeśli zmienisz segment danych na 1500 bajtów, rozmiar programu będzie większy o 1000 bajtów. Dane są montowane w rzeczywistym programie.

To właśnie się dzieje, gdy kompilujesz języki wyższego poziomu. Rzeczywisty obszar danych jest przydzielany podczas kompilacji do programu wykonywalnego, co zwiększa rozmiar programu. Program może również żądać pamięci w locie, a to jest pamięć dynamiczna. Możesz zażądać pamięci z pamięci RAM, a procesor da ci ją do użycia, możesz ją puścić, a twój garbage collector zwolni ją z powrotem do CPU. W razie potrzeby można go nawet zamienić na dysk twardy przez dobrego menedżera pamięci. Te funkcje zapewniają języki wysokiego poziomu.


2

Chciałbym wyjaśnić te pojęcia za pomocą kilku diagramów.

To prawda, że ​​na pewno pamięci nie można przydzielić w czasie kompilacji. Ale co dzieje się w rzeczywistości w czasie kompilacji.

Oto wyjaśnienie. Powiedzmy na przykład, że program ma cztery zmienne x, y, z i k. Teraz, w czasie kompilacji, po prostu tworzy mapę pamięci, na której ustalane jest położenie tych zmiennych względem siebie. Ten diagram lepiej to zilustruje.

Teraz wyobraź sobie, że żaden program nie działa w pamięci. Pokazuję to dużym pustym prostokątem.

puste pole

Następnie wykonywane jest pierwsze wystąpienie tego programu. Możesz to wizualizować w następujący sposób. Jest to czas, w którym faktycznie przydzielana jest pamięć.

pierwsza instancja

Gdy uruchomiona jest druga instancja tego programu, pamięć będzie wyglądać następująco.

druga instancja

I trzecia…

trzecia instancja

Itd. itp.

Mam nadzieję, że ta wizualizacja dobrze wyjaśnia tę koncepcję.


2
Gdyby te diagramy pokazywały różnicę między pamięcią statyczną i dynamiczną, byłyby bardziej przydatne w IMHO.
Bartek Banachewicz

Celowo tego unikałem, żeby wszystko było proste. Skupiam się na tym, aby wyjaśnić tę zasadę w sposób klarowny, bez większego technicznego bałaganu. O ile jest to przeznaczone dla zmiennej statycznej. Ten punkt został dobrze ustalony w poprzednich odpowiedziach, więc to pominąłem.
user3258051

1
Ech, ta koncepcja nie jest szczególnie skomplikowana, więc nie rozumiem, dlaczego ma być prostsza niż powinna, ale skoro jest pomyślana tylko jako komplementarna odpowiedź, ok.
Bartek Banachewicz

Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.