ABONAMENTE VIDEO REDACȚIA
RO
EN
NOU
Numărul 150
Numărul 149 Numărul 148 Numărul 147 Numărul 146 Numărul 145 Numărul 144 Numărul 143 Numărul 142 Numărul 141 Numărul 140 Numărul 139 Numărul 138 Numărul 137 Numărul 136 Numărul 135 Numărul 134 Numărul 133 Numărul 132 Numărul 131 Numărul 130 Numărul 129 Numărul 128 Numărul 127 Numărul 126 Numărul 125 Numărul 124 Numărul 123 Numărul 122 Numărul 121 Numărul 120 Numărul 119 Numărul 118 Numărul 117 Numărul 116 Numărul 115 Numărul 114 Numărul 113 Numărul 112 Numărul 111 Numărul 110 Numărul 109 Numărul 108 Numărul 107 Numărul 106 Numărul 105 Numărul 104 Numărul 103 Numărul 102 Numărul 101 Numărul 100 Numărul 99 Numărul 98 Numărul 97 Numărul 96 Numărul 95 Numărul 94 Numărul 93 Numărul 92 Numărul 91 Numărul 90 Numărul 89 Numărul 88 Numărul 87 Numărul 86 Numărul 85 Numărul 84 Numărul 83 Numărul 82 Numărul 81 Numărul 80 Numărul 79 Numărul 78 Numărul 77 Numărul 76 Numărul 75 Numărul 74 Numărul 73 Numărul 72 Numărul 71 Numărul 70 Numărul 69 Numărul 68 Numărul 67 Numărul 66 Numărul 65 Numărul 64 Numărul 63 Numărul 62 Numărul 61 Numărul 60 Numărul 59 Numărul 58 Numărul 57 Numărul 56 Numărul 55 Numărul 54 Numărul 53 Numărul 52 Numărul 51 Numărul 50 Numărul 49 Numărul 48 Numărul 47 Numărul 46 Numărul 45 Numărul 44 Numărul 43 Numărul 42 Numărul 41 Numărul 40 Numărul 39 Numărul 38 Numărul 37 Numărul 36 Numărul 35 Numărul 34 Numărul 33 Numărul 32 Numărul 31 Numărul 30 Numărul 29 Numărul 28 Numărul 27 Numărul 26 Numărul 25 Numărul 24 Numărul 23 Numărul 22 Numărul 21 Numărul 20 Numărul 19 Numărul 18 Numărul 17 Numărul 16 Numărul 15 Numărul 14 Numărul 13 Numărul 12 Numărul 11 Numărul 10 Numărul 9 Numărul 8 Numărul 7 Numărul 6 Numărul 5 Numărul 4 Numărul 3 Numărul 2 Numărul 1
×
▼ LISTĂ EDIȚII ▼
Numărul 101
Abonament PDF

Alocarea memoriei în .NET

Gabriel Martin
.NET developer @ PitechPlus



PROGRAMARE

Când un developer este responsabil de alocarea și dezalocarea explicită a memoriei, vorbim de o gestionare manuală a memoriei. Cel mai mare avantaj este controlul deplin. Însă apare și cel mai mare dezavantaj: atenția developerului este împărțită între obiectivul principal, de business și administrarea memoriei programului.

Runtime-ul .NET - pe care îl vom detalia mai jos, preia responsabilitatea din urmă, oferă management automat al memoriei și își propune eliminarea problemelor obișnuite: uitarea dezalocării memoriei (memory leaks), accesarea memoriei invalide (colectată anterior) etc.

Există trei concepte, mai abstracte, ce se leagă de gestionarea automată a memoriei și anume: alocator, mutator (i.e. mutator) și colector. Un mutator, definit în termeni simpli, este entitatea responsabilă de execuția codului aplicației. Își primește numele din simplul fapt că mută (schimbă) starea memoriei - obiectele sunt alocate sau modificate, iar legăturile dintre ele sunt schimbate. Alocatorul reprezintă entitatea responsabilă de administrarea alocării și dezalocării dinamice a memoriei. Nu în ultimul rând, colectorul este entitatea responsabilă de execuția codului pentru colectarea memoriei care nu mai este folosită (mai exact, procesul de garbage collection).

Un sistem de operare oferă propriile mecanisme de alocare de memorie. Mediile negestionate (i.e. unmanaged) precum C/C++ depind direct de acele mecanisme pentru a-și obține memoria necesară. Mediul .NET introduce un nivel adițional între sistemul de operare și programul ce se execută, acesta fiind runtime-ul.

Când se inițializează un nou proces, se rezervă un spațiu contiguu de memorie pentru acesta. Acest spațiu este împărțit în diferite segmente de memorie, cele mai importante alcătuind așa numitul Managed Heap (heap gestionat). Alocatorul va aloca obiectele în acest heap. Colectorul va ține evidența accesibilității obiectelor aflate în acest heap și va revendica memoria ocupată de obiectele inaccesibile.

.NET implementează, intern, propriul mecanism de alocare de memorie. Este mult mai rapid decât a cere sistemului de operare memorie de fiecare dată când un obiect este creat.

Cum implementăm alocarea memoriei?

Implementarea se face pe baza a două metode cunoscute:

Alocare secvențială

Cea mai simplă și rapidă cale de alocare de memorie în cadrul acestor segmente este folosirea și incrementarea unui pointer ce indică "sfârșitul" memoriei curente. Acest pointer poartă numele de "pointer de alocare" (allocation pointer). Când se dorește crearea unui obiect, pointerul se mută cu un număr de byți corespunzător mărimii obiectului.

Să presupunem că sunt unele obiecte deja create. Pointerul de alocare indică locația sfârșitului zonei de memorie ocupată de acele obiecte. Începând din acea locație vor fi alocate viitoare obiecte. Când e necesară memorie pentru un nou obiect (A), pointerul de alocare specifică adresa noului obiect, iar alocatorul incrementează valoarea pointerului cu numărul de byți corespunzător.

Fiind un algoritm secvențial , acesta oferă localitate bună a datelor. Este și supra optimist pentru că are nevoie de o disponibilitate infinită a memoriei. Aici intervine colectorul: obiectele ce nu mai sunt folosite sunt colectate, iar memoria (partea din stânga a pointerului) este compactată. În acest mod, pointerul de alocare este "derulat înapoi".

Pentru ca un obiect să fie într-o stare "curată" (clean), memoria trebuie reinițializată (zeroed). În avans, CLR-ul reinițializează o zonă de memorie, sfârșitul acesteia fiind reprezentat de un pointer adițional (sub numele de limită de alocare). Zona de memorie dintre pointerul de alocare și limita de alocare se numește context de alocare și este locul unde are loc alocarea rapidă, optimistă a obiectelor.

În cazul în care contextul de alocare este insuficient ca spațiu pentru obiectele ce urmează a fi create, fie acesta este extins, fie se creează unul nou. Un context de alocare nu trebuie să fie neapărat la sfârșitul segmentului de memorie, ci poate fi și într-o zonă liberă între obiecte existente. Pointerii (alocare și limită) vor reprezenta acel spațiu liber.

Fiecare context de alocare are o afinitate a threadului. Toate threadurile gestionate (managed, ce execută cod .NET) din aplicație au propriile contexte de alocare - un plus pentru performanță, eliminând necesitatea sincronizării.

Pe parcursul execuției aplicației, există multiple contexte de alocare ce se află în același segment. O parte dintre acestea se vor afla la finalul segmentului, iar o altă parte vor utiliza spațiul liber dintre obiectele existente.

Un dezavantaj al alocării secvențiale este fragmentarea memoriei. "Găurile" (obiectele nefolosite, ulterior colectate) de memorie trebuie cumva eliminate. Colectorul va compacta obiectele existente, iar contextele de alocare vor fi reorganizate într-un mod natural, și anume la finalul segmentului memoriei.

Alocare free-list

Ideea de bază este una trivială. Când CLR-ul cere să fie alocați un număr de byți, alocatorul caută într-o listă de "spații libere" unul îndeajuns pentru a acomoda numărul de byți. Căutarea se poate face pe baza a două strategii:

Un Managed heap este împărțit fizic în Small Object Heap (SOH) și Large Object Heap (LOH). Primul este împărțit la rândul lui, doar logic, în "generații" (0, 1 și 2, primele două alcătuind așa zisul segment efemer). Orice obiect mai mare de 85000 de byți va fi alocat în LOH, restul fiind alocate în SOH (mai exact în segmentul efemer).

Există mici diferențe între alocările din SOH și cele din LOH. În C#, operatorul new este tradus în limbaj intermediar (limbaj folosit de CLR) într-o instrucțiune newobj. Compilatorul JIT (just in time) emite invocarea adecvată a metodei pentru instrucțiunea newobj în funcție de varii condiții.

Alocarea obiectelor mici se bazează în mare pe alocare secvențială. Obiectivul este alocarea obiectele în cadrul contextului de alocare. În cazul în care alocarea nu poate fi făcută, o nouă cale de alocare, mai înceată, va fi executată. De aceea, pentru obiectele mici, metoda invocată de JIT este cea eficientă, având ca beneficiu "alocare ieftină".

O perspectivă high-level a procesului de alocare în SOH:

Propoziția "Alocarea este ieftină în .NET" este adevărată până într-un punct. Alocările trebuie utilizate prudent și să nu aloce obiecte inutile. Într-o aplicație, în care performanța este critică, regula principală în ceea ce privește alocările este ca acestea să fie evitate.

Alocarea obiectelor mari (ce ajung în LOH) este bazată atât pe alocarea free-list, cât și pe o alocare secvențială mai simplificată (nu se folosește context de alocare).

Contextul de alocare și alte optimizări asociate nu sunt importante deoarece costul ștergerii unui obiect mare este dominant. Nu există diferență între alocarea rapidă și alocarea slow-path. Aceiași pași sunt executați, fiind asemănători cu pașii alocării slow-path în SOH:

Deși nu se folosește contextul de alocare în LOH, pentru obiectele noi, memoria tot trebuie reinițializată, proces ce este costisitor pentru obiectele mari. De aceea, ca regulă, se încearcă evitarea excesivă a alocărilor în LOH și, în schimb, se preferă crearea unei rezerve de obiecte reutilizabile (reusable objects pool).

Memoria este și va fi întotdeauna o resursă limitată și mereu vor exista erori sau modalități de utilizare incorectă. Singurul lucru care se va schimba mereu privind gestionarea memoriei este cantitatea. Dimensiunea memoriei va crește, timpul de acces va scădea, făcând posibilă prelucrarea datelor într-un timp satisfăcător. Într-un articol viitor vom vorbi și despre alocarea pe stack și evitarea alocărilor.

Bibliografie:

  1. [Kokosa, Konrad - Pro .NET Memory Management: For Better Code, Performance, and Scalability](https://prodotnetmemory.com/ ) (cap. 6 Memory Allocation)
  2. The Book Of Runtime - Garbage Collection Design
  3. Automatic Memory Management
  4. .NET Memory Management Concepts - Help | dotMemory

NUMĂRUL 149 - Development with AI

Sponsori

  • Accenture
  • BT Code Crafters
  • Accesa
  • Bosch
  • Betfair
  • MHP
  • BoatyardX
  • .msg systems
  • P3 group
  • Ing Hubs
  • Cognizant Softvision
  • Colors in projects