Sunday, October 20, 2019 14:18

Memoria Stack vs Heap

Astăzi vom vorbi despre memorie. Există mai multe tipuri de memorie atunci când vine vorba de software, dar pentru moment suntem interesați doar de două dintre ele: Stack și Heap. Ori de câte ori executăm un program, instrucțiunile sunt încărcate în memoria RAM a calculatorului, iar sistemul de operare va aloca o porțiune de RAM fizic, astfel încât executabilul să poată rula. Așa cum veți afla în această lecție, Stack și Heap sunt două zone de memorie situate în memoria RAM a computerului. Există mai multe diferențe între aceste două tipuri de memorie, atât din punct de vedere al caracteristicilor, cât și a funcționalității, iar prima dintre acestea este mărimea. Stack este o memorie cu dimensiune predefinită, cu o capacitate de aproximativ 2 megaocteți, în timp ce Heap are și ea un fel de dimensiune predefinită, dar este mult mai mare și se poate extinde pe măsură ce execuția impune. Deci, în primul rând, de ce avem nevoie de memorie, de ce avem nevoie de RAM? Motivul pentru care programele necesită memorie pentru a rula este acela că ele au nevoie să stocheze și să proceseze date. Ar fi un proces foarte ineficient să stocăm, citim, modificăm și să scriem toate aceste date pe hard disk. Deci, RAM-ul permite programelor noastre să stocheze date din variabile și obiecte, în timp ce permite modificarea și procesarea lor într-un mod foarte eficient. Chiar dacă ambele furnizează în mod fundamental aceeași funcționalitate, Stack și Heap sunt foarte diferite în modul în care operează.

Stack este în general folosit pentru a ține o evidență a apelurilor de metode și funcții făcute în programul nostru. Ori de câte ori apelăm o metodă sau o funcție, respectivul apel este stocat în interiorul Stivei. De asemenea, Stiva este un bloc de memorie contiguu (continuu, în șir). Din aceste motive, vă puteți gândi la Stack ca fiind o grămadă de cutii, una peste cealalaltă. Nu putem pune o cutie direct în mijlocul stivei, așa cum nu putem lua o cutie fără a dărâma toată grămada. Ori de câte ori adăugăm o cutie, o adăugăm în partea superioară a grămezii, și doar acolo; la fel și atunci când luăm o cutie – luăm doar una câte una, începând din capătul de sus.

The Stack and the Heap

Pentru a vizualiza mai bine acest lucru, să luăm următorul cod:

Cod foarte simplu: avem metoda Main() care apelează o metodă numită PrimaMetoda(), care apelează o metodă numită ADouaMetoda(), care apelează din nou o metodă numită ATreiaMetoda(), care nu face nimic.

Setați un breakpoint la începutul metodei Main() și la sfârșitul celorlalte metode, astfel:

Rulați programul. Așa cum era de așteptat, atunci când execuția va atinge breakpoint-ul din metoda Main(), execuția va fi întreruptă și vom intra în modul de depanare. Nimic nou până acum. Dar ceea ce ne interesează este un panou numit Call Stack, situat în partea de jos a ferestrei Visual Studio. Dacă nu îl găsi, acesta poate fi afișat accesând meniul Debug, submeniul Windows și selectând Call Stack:

Visual Studio afisarea panoului Call Stack

Rețineți că acest panou este disponibil numai în modul de depanare, astfel încât să nu vă mire dacă nu îl veți găsi în timp ce programul vostru se execută sau este oprit în modul de proiectare.

Deci, să aruncăm o privire la acest panou. Când se atinge primul breakpoint, cel din metoda Main(), vom obține aceasta:

Visual Studio Call Stack

Vom vedea un breakpoint similar cu cel care este activ în momentul de față (unde execuția a fost pauzată), dar cu unele informații suplimentare în vecinătatea sa, cum ar fi numele modulului (metodele sunt stocate în clase, clasele sunt stocate în module, modulele sunt stocate în ansambluri, dar nu trebuie să vă faceți griji despre asta acum), numele assembly-ului nostru, clasa și metoda care a fost apelată, plus parametrii săi, și alte lucruri precum limbajul de programare utilizat în cod și numărul liniei unde a fost apelată metoda.

Apăsați butonul Continue și veți vedea că execuția este transferată metodei ATreiMetoda(), întrerupându-se la breakpoint-ul de la sfârșitul acesteia. Știm deja din lecțiile anterioare că acest lucru se întâmplă deoarece apelul metodelor este executat înainte de terminarea execuției blocurilor metodelor de unde se apelează celelalte metode, astfel că, urmând logica programului, execuția a sărit de la metoda Main() la PrimaMetoda(), apoi la ADouaMetoda() și în cele din urmă la ATreiaMetoda(), la finalul căreia s-a oprit, acolo unde a fost setat breakpoint-ul. Dacă trebuie să urmați această logică pas cu pas, puteți reporni execuția de la primul breakpoint și să apăsați în mod constant Step Into, pentru a vedea fluxul execuției.

Acum, când suntem la sfârșitul metodei ATreiaMetoda(), să aruncăm din nou o privire la Call Stack:

Putem vedea în mod clar că avem 4 apeluri, începând cu apelul Main() din partea de jos (metoda Main() a fost apelată de Common Language Runtime, CLR) și terminând cu ATreiaMetoda() în partea de sus. Acest lucru este în perfectă concordanță cu ceea ce am spus mai devreme, că Stack-ul poate fi privit ca o grămadă de cutii, una peste alta. Dacă veți continua să apăsați pe Continue, veți vedea execuția sărind și oprindu-se la breakpoint-urile din ADouaMetoda(), PrimaMetoda() și, în final, revenind la Main(), unde va ajunge la instrucțiunea Console.Read(). În același timp, vom vedea Call Stack urmând aceeași cale, eliminând apelurile unul câte unul, pe măsură ce acestea se termină. Cu alte cuvinte, Call Stack păstrează o evidență a tuturor apelurilor de metode pe care le facem în programul nostru, adăugându-le atunci când sunt apelate și eliminându-le atunci când se termină. Dacă vă mai amintiți, acest lucru nu este foarte diferit de structura de date Stack; De fapt, Stack-ul în sine poate fi privit exact ca această structură de date, cu singura diferență că nu are o funcționalitate generală de stocare – stochează doar apeluri de metode și este complet automatizat – nu permite utilizatorului intervenţia directă.

O altă caracteristică utilă a Call Stack, pe lângă faptul că putem vedea întreaga cale urmată de apelurile făcute în programul nostru, este că putem efectua dublu clic pe unul din rânduri, ceea ce va face execuția să revină la apelului respectiv. Acest lucru ar putea fi util uneori.

Uneori, în programele GUI și mai ales atunci când veți folosi biblioteci externe (vom afla despre adăugarea referințelor de librarii în viitor), veți observa că Call Stack va conține o mulțime de apeluri pe care nu le-ați făcut personal. De exemplu, dacă aveți un buton și stabiliți un breakpoint în operatorul de evenimente al evenimentului Click, veți observa că atunci când breakpoint-ul va fi atins, vor exista o mulțime de apeluri în Call Stack, terminând cu apelul la metoda Click(), în vârf. Apelurile suplimentare sunt doar rezultatul abstractizării și stratificării software-ului. Spre deosebire de exemplul nostru, în care codul este foarte direct și simplu, un manipulator de eveniment Click solicită de fapt O MULȚIME de alte metode de nivel inferior, în fundal. Toate apelurile respective sunt afișate în Call Stack și este posibil să nu fiți interesați de acestea.

De asemenea, uneori veți vedea rânduri în Call Stack în genul [cod extern] ([external code]). Acestea sunt doar apeluri care au fost încapsulate și ascunse de utilizator. Pentru moment nu este necesar să vă faceți griji în privința acestora.

Stack-ul și Heap-ul sunt zone de memorie. Ele sunt folosite doar pentru a stoca lucruri și, mai precis, tipuri de valoare, tipuri de referință, pointeri și instrucțiuni. Tipurile de valoare sunt bool, byte, char, decimal, double, enum, float, int, long, sbyte, short, struct, uint, ulong și ushort. Acestea sunt tipuri de valoare deoarece sunt declarate în spațiul de nume System.ValueType. Tipurile de referință sunt class, interface, delegate, object și string; toate acestea moștenesc din System.Object, cu excepția object, care este obiectul System.Object însuși.

Tipurile de referință sunt întotdeauna stocate în Heap. Și, din păcate, dacă veți face o căutare rapidă pe Google, veți găsi o mulțime de exemple care vă vor spune că diferența dintre Stack și Heap este faptul că tipurile de referință sunt stocate în Heap, în timp ce tipurile de valoare sunt sunt întotdeauna stocate în Stack. Chiar și documentația Microsoft spune asta. Cu toate acestea, ACEST LUCRU NU ESTE ÎN ÎNTREGIME ADEVĂRAT! Tipurile de valoare sunt stocate în Heap, Stack sau Registers (un alt tip de memorie), în funcție de locul în care sunt declarate și de durata vieții lor. Dacă sunt parametri de metode și variabile locale ale unei anumite metode sau funcții, ele sunt stocate în Stack, care este și cazul cel mai întâlnit. Dacă sunt declarate direct în interiorul unui tip de referință (cum ar fi declararea unui int direct în interiorul unui tip de referință, cum ar fi o clasă, cunoscută și ca declarare a unei variabile de câmp), ele sunt stocate pe Heap, împreună cu tipul de referință care le înglobează.

Majoritatea programatorilor vă vor spune că o altă diferență între Stack și Heap este că interacțiunea cu valorile stocate pe Stack este mai ușoară și mai rapidă decât cele stocate în Heap. Acest lucru este doar parțial adevărat. Diferența de performanță între declararea unei tip prin valoare și a unuia prin referință este neglijabilă. Diferența în interacțiunea cu valorile din Stack și Heap este, de asemenea, neglijabilă (în majoritatea cazurilor!). Deci, singura diferență reală este atunci când valorile nu mai sunt necesare și trebuie eliminate. Deoarece Stack-ul este un mediu de stocare liniar, costul de performanță al ștergerii variabilelor și recuperarea memoriei este mic, deoarece blocurile de memorie nu necesită rearanjarea lor pentru a umple golurile; memoria ștersă este întotdeauna în partea de sus a stivei. Cu toate acestea, pe Heap, deoarece valorile sunt stocate într-un mod împrăștiat, când nu mai sunt folosite (un proces complex care implică pointeri, despre care nu am discutat încă), acestea sunt șterse de o componentă specială a .NET Framework , numit colector de gunoi (GC pe scurt, din englezescul garbage collector). GC nu va șterge tot timpul obiectele neutilizate din memorie; va face acest lucru la anumite intervale, care este un subiect complex. Cu toate acestea, atunci când memoria este curățată, vor apărea găuri în ea care trebuie umplute; astfel, GC va re-aranja toate valorile din Heap astfel încât acestea să fie din nou într-o structură contiguă. Acest proces este unul foarte costisitor din punct de vedere al performanței. Cu toate acestea, ceea ce majoritatea programatorilor nu știu este faptul că memoria Heap este împărțită în trei zone: zone din Heap cu durată de viață scurtă, medie și lungă; obiectele își încep existența în zona cu durată scurtă de viață. Dacă supraviețuiesc unei colecții de gunoi (adică dacă sunt încă necesare software-ului), acestea sunt mutate în zona cu durată de viață medie. În mod similar, dacă supraviețuiesc încă unei curățări, ele sunt mutate în zona cu durată lungă de viață. În acest fel, memoria rămâne oarecum mai organizată, blocurile cu durate lungi și medii de viață fiind rearanjate relativ rar. Singura zonă care este foarte costisitoare în ceea ce privește colectarea gunoiului este zona cu durată scurtă de viață.

Acesta este motivul pentru care majoritatea programatorilor au oarecum dreptate. Heap-ul poate fi mai costisitor de utilizat, din punct de vedere al performanței, dar numai dacă obiectele pe care le alocăm acolo sunt numeroase și au durată de viață scurtă.

Este păcat că majoritatea programatorilor, inclusiv cei profesioniști, consideră că tipurile de valoare sunt mai indicate pentru folosirea curentă, pentru singurul motiv de a fi alocate pe Stack, nu pe Heap. Nu numai că acest lucru nu este în întregime adevărat, dar întreaga idee este greșită. Caracteristica relevantă a tipurilor de valoare este aceea că ele au semantica de a fi copiate prin valoare, nu aceea că uneori dealocarea lor poate fi optimizată de runtime. În caz contrar, dacă aceasta ar fi caracteristica relevantă pentru tipurile de date, acestea ar fi denumite tipuri de stivă și tipuri de heap, nu tipuri de valoare și de referință.

Comments

comments

Tags: , , , , , ,

2 Responses to “Memoria Stack vs Heap”

  1. Victor spune:

    Foarte rar gesesti pe cineva care sa explice atat de „pe intelesul tuturor”! Multumesc!

Leave a Reply