jak přidělit více beranu na rez


Odpověď 1:

Stručně řečeno, zásobník je jednoduchý a rychlý, ale jeho velikost je omezená a alokace bude trvat pouze do konce funkce. Halda je mnohem větší, lze ji použít k přidělení dlouhodobé a velké části paměti a vyžaduje více správy, takže je přidělování poměrně pomalé.

Proč není pro zásobník potřeba žádná alokace / deallocation paměti?

Zásobník volání, obecně nazývaný „zásobník“, je nezbytnou součástí volání funkce. Používá se k udržování, dobře, skládání proměnných a aktuálního stavu hovoru.

Obecně je paměť použitá pro zásobník přidělena s určitou konkrétní pevnou velikostí na začátku procesu / vlákna a je používána po celou dobu její životnosti. Proto se říká, že není potřeba žádné „přidělování paměti“. Ve skutečnosti se to také počítá jako

druh alokace paměti

, ale alokace v zásobníku je příliš jednoduchá a příliš rychlá ve srovnání s alokací na základě haldy, takže máme tendenci říkat, že není potřeba žádná „alokace“.

Proč také nemůže kompilátor vědět, kde bude objekt v haldě?

Když to řeknete, možná jste již věděli, jak zásobník funguje. Halda nefunguje jako hromádka. Halda je v podstatě hromada paměti, kterou lze každou část alokovat a uvolnit v neurčeném pořadí. Přemýšlejte o tom jako o velmi dlouhém (ale konečně dlouhém) vlaku, který může každý cestující obsadit určitým počtem sousedících podvozků. Cestující budou nastupovat a vystupovat mezi stanicemi bez jakéhokoli známého plánu nebo vzorů. Správa haldy je něco jako správa těchto cestujících, aby získali tolik podvozků, které potřebovali, aby mohli nastoupit do vlaku, což vyžaduje sledování všech dostupných podvozků a nějaký algoritmus, který jim pomůže najít nejvhodnější podvozky, aniž by jim došlo souvislé podvozky z dlouhodobého hlediska. To je mnohem složitější a časově náročnější než vkládání některých dat do zásobníku volání.

Jak může kompilátor vědět, kde bude objekt, pokud je uložen v zásobníku?

Nové objekty, které patří do zásobníku volání, budou vždy uloženy v horní části zásobníku volání. To je velmi optimalizované, protože operace zásobníku jsou nativně podporovány ve většině, ne-li všech typech procesorů, které poskytují alespoň jeden vyhrazený vnitřní registr pro „stack pointer“ - ukazatel, který ukazuje na horní část zásobníku volání. Kompilátor ve skutečnosti neví, kam uložit objekt v zásobníku, ale určitě může vytvořit instrukci, jak to udělat.

Velkým varováním je, že i když jsme si jisti, že zásobník je ve většině případů rychlejší než halda, mluvíme zde o rozdílu v nanosekundách na alokaci. Nepokoušejte se provést tento druh optimalizace pro žádnou triviální funkci.


Odpověď 2:

Pokusme se v jednoduché angličtině vysvětlit, co je zásobník a co halda.

Zásobník je hromádka paměťových bloků, které jsou vytvořeny při volání funkce. Takže pokaždé, když program C ++ zavolá funkci (libovolnou funkci. Dokonce main ()), přidělí této paměti blok paměti na zásobníku.

Tento blok paměti obsahuje všechny proměnné vytvořené na této funkci a také adresu původní funkce, která ji volala. Když provádění této funkce skončí, automaticky uvolní tento blok paměti a účinně vymaže i proměnnou, kterou funkce vytvořila staticky (libovolný int, char, float, cokoli). Vzhledem k tomu, že překladače již předem vědí, kde bude každá vytvořená proměnná umístěna do paměti, může provést mnoho optimalizací.

POKUD chcete vytvářet dynamické proměnné. Věci jako ukazatel (nebo dokonce vektor) . V tomto případě nebudou prvky přiděleny například na zásobníku). Představme si následující situaci (je to prosté C, ale bude to fungovat kvůli vysvětlení):

void function (int m) { int * v = (int *) malloc (m * sizeof (int)); // něco s tím udělejte}

Pokud nemůžete přečíst kód, jednoduše vytvoří pole ints o velikosti m pomocí ukazatele * v. V tomto případě kompilátor nemá žádný způsob, jak předem zjistit velikost pole, protože m bude známé pouze tehdy, když program skutečně běží. Jak tedy kompilátor ví, jak velký musí být blok paměti pro tuto funkci v zásobníku, když je volán?

Odpověď zní: Není. V tomto případě (a ve skutečnosti v jakémkoli jiném případě, že používáte ukazatele, i když implicitně, jako ve std :: vektoru ), skutečné pole nebude uloženo v zásobníku, ale v hromadě, což je jiná oblast paměti, nezávislá na zásobníku. Kompilátor tedy přidělí pouze dostatek paměti pro POINTER, nikoli data (v tomto případě pravděpodobně 32bitové celé číslo), a samotná data budou žít v jiné části paměti s adresou uloženou v ukazateli ( pro puristy kolem je ukazatel adresa prvního prvku pole). Problém s tím je, že když se funkce vrací, nemá žádné informace o samotném poli, takže uvolnění je povinností programátora a kompilátor prostě nemůže optimalizovat tento proces tak efektivně. Pokud to programátor neudělá správným způsobem, dojde k úniku paměti.

Dobrá část je, že protože data nejsou v zásobníku, ale v hromadě, nebudou zničena, jakmile se funkce vrátí. Ukazatel dat tedy můžete snadno předat dalším funkcím, které nyní mohou přistupovat k těmto datům, která žijí v haldě.

Celá tato konverzace je v zásadě platná pro obyčejný C. Ale v pozadí mnoha datových struktur v C ++ se děje totéž. Například C ++ std :: vector přidělí každý prvek vektoru na haldě, nikoli na zásobníku, protože nemá žádný způsob, jak předem vědět, kolik prvků může vektor nakonec dostat.

Na mém vysvětlení je pravděpodobně několik nedostatků. Neodpověděl jsem to proti čtení, ale hlavní myšlenkou je tato.

tl; dr: Proměnné, které jsou pro funkci lokální, jsou přiděleny na zásobník a automaticky se uvolní, když se funkce vrátí. Proměnné, které kompilátor nemůže předem odvodit velikost (například vektory) nebo které jsou ukazatele (které pravděpodobně použijí jiné funkce), jsou přiděleny na haldu.


Odpověď 3:

Hlavním důvodem je, že se zásobník vejde do místní mezipaměti CPU.

Představte si deklaraci int i; proměnná; řekněme v jednoduchém cyklu for, například for (int i = 0; i

Dokonce i tvrdý člověk z Javy by si uvědomil, že přidělením i „externě“ (efektivně, vytvořením nějakého Int i = new Int (); call) plýtváte prostředky.

Řekněme, že máte třídu, která je několik - nebo dokonce jen jedno - celé číslo velké. Proč byste jej na Zemi chtěli přidělit pomocí nového, pokud existuje snadný způsob?

Tento snadný způsob je možný - a široce podporovaný - v C ++, protože místo uvolňování paměti založeného na počítání referencí C ++ používá správu životnosti objektu založenou na oboru. Kompilátor ví, že jakmile skončí aktuální obor, měly by být v něm automaticky přiděleny objekty. Rozložení paměti pro lokálně přidělené objekty se tak pohodlně shoduje s tím, jak zásobník funguje pro volání vnořených funkcí.

Toto pohodlí samozřejmě není náhoda. Je to filozofie toho, jak C ++ pracuje s životy objektů.

Nevýhoda: Když je potřeba „flexibilita“ správy životnosti objektu, prosté automaticky přidělené objekty nebudou fungovat - protože jsou přiděleny na zásobníku, již nebudou platné, protože volaná funkce opustí svůj rozsah. Programátor tedy musí výslovně deklarovat tyto objekty „perzistentní“ ve větším měřítku - nejčastěji pomocí inteligentních ukazatelů, i když ve skutečnosti jde o pole, kde jsou nezbytné holé ukazatele a hrubý nový / mazaný.

(Toto je, mimochodem, jedna častá úskalí C ++: předání ukazatele / odkazu na objekt přidělený zásobníku do asynchronního volání, aniž by bylo zajištěno, že přidělený rámec zásobníku bude žít dostatečně dlouho. Futures a sliby FTW v tomto případě. )

Vzhůru: Výkon, technická krása a pozoruhodná předvídatelnost správy vlastnictví objektu.

Pokud jde o samotnou alokaci paměti, jde o relativně malý nárůst výkonu ve srovnání s lokalitou dat pro lepší ukládání do mezipaměti. Je pravda, že samotná paměť nemusí být „alokována“ pro objekty přidělené zásobníku, nicméně uživatelsky definované konstruktory a destruktory pro objekty přidělené zásobníku jsou samozřejmě volány.

Současně je fragmentace paměti, která je takovou bolestí pro jazyky shromažďované odpadky, problém, který pro alokaci založenou na zásobníku a správu životnosti zcela neexistuje.

Oh, a zmínil jsem se o tomto přidělení na základě rozsahu, že konečná konstrukce je naprosto zbytečná? No ano.


Odpověď 4:

Může být (i když v Javě často nemáte na výběr.)

Zde je jednoduché odůvodnění: alokace ze zásobníku je pouze otázkou úpravy registru, zatímco alokace z haldy zahrnuje volání funkce. I když je volání funkce podřízené, haldy datových struktur jsou často netriviální (ve snaze optimalizovat umístění nových objektů, aby nedocházelo k fragmentaci.)

Cvičení: Zkompilujte program C ++ pomocí alloca a jednoho pomocí malloc a podívejte se na rozdíly ve výsledném kódu.

Nyní v systému sběru odpadků může být téměř stejně rychlé přidělit z haldy, protože haldu lze uspořádat jako

Regionální alokátor

. Ale i tak obvykle existují některé objekty pro sledování metadat a určitá synchronizace, aby bylo možné z hromady přidělit více vláken. Alokace na zásobníku nepotřebuje ani jednu z nich, protože po skončení funkce vše „vypneme“ a každé vlákno již má svůj vlastní zásobník.

Lokalita paměti může navíc fungovat spíše ve prospěch alokace ze zásobníku než z haldy. Paměť použitá pro zásobník vláken může již být v mezipaměti L1 nebo L2 CPU. Je také nepravděpodobné, že by se chovalo „ping pong“ s jiným CPU, protože žádné jiné vlákno by nemělo důvod pro přístup k zásobníku aktuálního vlákna.


Odpověď 5:

Záleží na implementaci haldy. Pokud neexistuje správa haldy, pak není žádný rozdíl, ale nakonec vám dojde hromada prostoru, tím více jej využijete. Viděl jsem jednoduché implementace, kde zásobník přechází z nízkých adres na ty vyšší v paměti a hromada jde z vysoké na nízkou a hromada stále roste, čím více se používá, dokud se nespustí do zásobníku.

Halda paměti je udržována odděleně od paměti zásobníku, takže se nemusí obávat, že se obejdou.

Ve většině moderních systémů je halda spravována. Sleduje nepřidělenou paměť a používá / znovu ji používá, když je požadováno přidělení. Častější implementace zachází s nepřidělenou hromadou jako s propojeným seznamem. Na základě požadované velikosti paměti halda hledá volné místo, které ji může pojmout, a označí ji jako přidělenou. Vzhledem k tomu, že hledání haldy volného místa, které bude mít správnou velikost, je lineární proces, trvá to trochu času. Trvá více času, pokud je na haldě přidělena a přidělena spousta malých částí na haldě, protože to způsobí, že v haldě bude více volných míst k prohledávání v seznamu. Tomu se říká fragmentace haldy. Existují optimalizované strategie pro alokaci / deallokaci haldy paměti, které by to měly minimalizovat, ale faktem je, že pouze první alokace haldy bude tak rychlá jako operace na zásobníku.

Zásobník přiděluje paměť v konstantním čase. Přidělování a přidělování prostoru zásobníku je jednoduché. Chcete-li přejít na zásobník, jednoduše přesune ukazatel zásobníku za rámec zásobníku a vytvoří nový. Chcete-li zrušit přidělení, přesune ukazatel zásobníku zpět na začátek předchozího rámce zásobníku. Nemusí se starat o mezery v přidělené paměti, jak to dělá halda.


Odpověď 6:
Zásobník je rychlejší než halda, protože je zaručeno, že paměť zásobníku bude uvolněna v opačném pořadí, než je přidělena. Díky tomu je mnohem snazší spravovat (není třeba například slučovat volné oblasti) a optimalizuje lokalitu přístupů do paměti.

Odpověď 7:

Přidělování zásobníku je mnohem rychlejší, protože vše, co opravdu dělá, je pohyb ukazatele zásobníku. Pomocí fondů paměti můžete z alokace haldy získat srovnatelný výkon, ale to přichází s mírnou přidanou složitostí a vlastními bolestmi hlavy. Stack vs. halda také není jen úvahou o výkonu; také vám řekne hodně o očekávané životnosti objektů. Velikost zásobníku je konečná, protože rychle zjistíte, zda nadměrně nepoužíváte přidělení zásobníku. Alokace zásobníku je vhodnější pro dlouho běžící serverové aplikace. Dokonce i ty nejlépe spravované hromady se nakonec tak roztříští, že se výkon aplikace sníží.


Odpověď 8:

Většina odpovědí je příliš zachycena v abstrakcích.

Alokace zásobníku se provádí na úrovni CPU jednoduchým dílčím návodem na ukazatel zásobníku.

Alokace haldy je na druhé straně spravována operačním systémem nebo jinou abstrakcí, která „spravuje“ paměť.

Co je rychlejší - jediná instrukce CPU nebo sada operací prováděných voláním algoritmu správy paměti operačního systému? ;)

Přidělení haldy se provádí za běhu, takže neexistuje způsob, jak kompilátor zjistit, kde se v haldě něco přidělí.


Odpověď 9:

Rozložení zásobníku je nastaveno v době kompilace, halda v době běhu. Základní alokace je tedy v zásobníku poněkud rychlejší, ale ten čas je obvykle převyšován dalšími kroky při konstrukci objektů, jako je spuštění konstruktoru.