August 28, 2023

CPP - Stack vs Heap

הקדמה

ניהול זיכרון בשפת C++ הוא בין הנושאים הקריטיים ביותר שיש להבין אותם.
לכן כאשר מתבצעים הקצאות יש להבין כיצד יש לעשות את ההקצאה והאם יש דרך יותר טובה לעשות אותה?

איך לשמור על זיכרון ואיך למנוע טעויות בשימושו?

בפוסט הקצר הזה נלמד על מהי מחסנית, מה זה זיכרון Heap ומה ההבדל בין השניים.

מה זו מחסנית

מחסנית בהגדרתו הוא מבנה נתונים מסוג של LIFO - Last in First out.
האיבר האחרון שהכנסנו הוא יהיה האחרון שייצא.

אולם, המחסנית כזיכרון הוא אזור זיכרון קטן שמוקצה מראש לדחיפה והוצאה של זיכרון בצורה מלאה.

מאפיינים של המחסנית

  • הקצאת זיכרון מהירה: כוללת הזזה בלבד של מצביע במחסנית.
  • גודל מוגבל: בדרך כלל הגודל קבוע מראש לכל מחסנית.
  • שחרור אוטומטי: מכיוון שהקצאה כוללת רק הזזה של מצביע - השחרור הוא כמעט אוטומטי ע”י הזזה חזרה של המצביע.

דוגמת קוד

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <iostream>

void UseStackMemory()
{
int stackArray[10]; // מערך בגודל 10 מוקצה במחסנית

stackArray[0] = 1;

std::cout << "Stack array first element: " << stackArray[0] << "\n";
}

int main()
{
UseStackMemory();
return 0;
}

טעות הנפוצה עם המחסנית

הקצאה גדולה מדי של זיכרון או לחילופין קריאה רקורסיבית לפונקציה שאינה נגמרת תנצל את המחסנית עד הסוף.
שגיאה כזו נקראת Stack overflow exception.

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <iostream>

void CauseStackOverflow(int count) {
int largeArray[100000]; // יגרום לשטפון מחסנית אם יקרא יותר מדי פעמים
std::cout << "Stack frame: " << count << std::endl;
CauseStackOverflow(count + 1);
}

int main()
{
CauseStackOverflow(1);
return 0;
}

מהו ה-Heap

זהו אזור זיכרון הרבה יותר רחב שמוקצה ממערכת ההפעלה לאפליקציה.
במערכות הפעלה 32bit האזור היה אכן מוגבל רק ל-4 ג’יגה.
בדרך כלל 2 היה מוקצה עבור מערכת ההפעלה ועוד 2 ג’יגה מוקצים לאפליקציה אז כמות הזיכרון הייתה עוד יותר מוגבלת.

במערכת הפעלה 64bit הגודל גדל למימד עצום מבחינה מעשית ולכן אין כל בעיה כיום.

מאפיינים

  • גודל דינאמי - ניתן להקצות עוד ועוד זיכרון כל עוד יש זיכרון פיזי שיכול לגבות אותו.
  • שחרור חצי ידני - יש לשחרר זיכרון באופן ידני או במכניקה אחרת כגון RAII.
  • הקצאות איטיות - מכיוון שזהו אזור גדול שיש לגדר אותו ולתפעל אותו ההקצאה יותר איטית.
    בדרך כלל זה צורך מהמערכת הפעלה לטעון קטעי זיכרון שלא טעונים לגישה מידית.
    זה נקרא עקרון ה-Cache.

דוגמת קוד

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <iostream>

void UseHeapMemory()
{
int* heapArray = new int[10]; // מערך בגודל 10 מוקצה בזיכרון דינאמי

heapArray[0] = 1;
std::cout << "Heap array first element: " << heapArray[0] << std::endl;

delete[] heapArray; // שחרור הזיכרון
}

int main()
{
UseHeapMemory();
return 0;
}

כדי להקצות באופן דינאמי יש להשתמש במצביע - int* ולבצע הקצאה בעזרת new int[n].
כדי למחוק יש לבצע delete[] ptr או delete ptr במידה וזה לא מערך.

כיום יש כמה טכניקות להימנע מהקצאות ידניות כאלו.

  1. Smart pointers
C++ Few tips to go by
  1. שימוש במבנים מוגדרים כגון std::vector למערך דינאמי.

  2. שימוש ב-RAII ואנקפסולציה של הקצאות לתוך מחלקות בשימוש ב-DTOR.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    #include <iostream>

    class MyResource
    {
    public:
    MyResource()
    {
    mResource = new int(5);
    }

    ~MyResource()
    {
    delete mResource;
    }

    int Get() { return *mResource; }

    private:
    int* mResource;
    };

    int main()
    {
    {
    MyResource resource;
    std::cout << "Resource is: " << resource.Get() <<"\n";
    }
    // Out of scope - resource is deleted
    }

טעויות נפוצות עם זיכרון דינאמי

  • דליפות זיכרון: כאשר שוכחים למחוק את הזיכרון שהקצאנו הזיכרון יישאר עד שתיסגר התוכנה.
  • הקצאות מרובות מדי בקטעי קוד קריטיים: מכיוון שהקצאות הן איטיות, קוד קריטי צריך לרוץ עם זיכרון מוקצה מבעוד מועד.
  • אם למחלקה אין גודל מסוים, אין צורך בהקצאה דינאמית אלה אם כן רוצים לשתף את המופע.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
void MemoryLeak()
{
int* a = new int[10];

// oops forgot to clear a
}

void SlowAlloc()
{
int* a;
for(int i=0 ; i<10 ; i++)
{
a = new int[10]; // If allocated and resued - faster
for(int j=0; j<10;j++)
{
a[j] = (j + 1);
}
}
}

class Doer
{
public:
void Do() { }
};

void NoNeedToAllocate()
{
Doer* doer = new Doer();
auto doer2 = std::make_unique<Doer>();
delete doer;
}

int main()
{
MemoryLeak();
SlowAlloc();
NoNeedToAllocate();
}

השוואה

מחסנית Heap תכונה
מהיר איטי מהירות
מוגבל דינאמי גודל
אוטומטי ידני שחרור
משתנים מקומיים או חסרי גודל מבנים מורכבים ודינאמיים שלא יודעים כמה ייקח זיכרון לפני שימוש

סיכום

בעוד שהמחסנית מהירה ומנהלת זיכרון אוטומטית הגודל המוגבל שלה הופך אותה ללא מתאימה למבנים מורכבים או גדולים.
זיכרון דינאמי מציע גמישות אך זה גורר הקצאות איטיות יותר והצורך לנהל את הזיכרון.

הבנה עמוקה של מבנה הזיכרון ותפקיד של כל מבנה זיכרון ייעזור בבניית תוכנות יעילות שלא מבזבזות משאבים!

בונוס - זיכרון דינאמי במערכות הפעלה

רוב הזיכרון שנצרוך הוא מהזיכרון הדינאמי.
להבין איך מערכות הפעלה עובדות זה די ה-101 בשביל להבין איך לעבוד עם זיכרון.

בווינדוס יש לנו זיכרון דינאמי שנקרא - Low Fragmentation Heap.
אחרי ויסטה זה נמצא בשימוש תמידי בתוך Processes.

בלינוקס יש שימוש במבנה אחיד ויחיד - Single Heap.
ניתן כמובן לעבוד עם Sub-Processes בצורות שונות בהתאם לצרכי התוכנה.

Windows - VMMAP by SysInternals

Linux - pmap

בלינוקס נוכל להשתמש ב-pmap על מנת לקבל תמונת מצב:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
ILUGA:~$ ps
PID TTY TIME CMD
9 pts/0 00:00:00 bash
98 pts/0 00:00:00 ps
ILUGA:~$ pmap bash

Usage:
pmap [options] PID [PID ...]

Options:
-x, --extended show details
-X show even more details
WARNING: format changes according to /proc/PID/smaps
-XX show everything the kernel provides
-c, --read-rc read the default rc
-C, --read-rc-from=<file> read the rc from file
-n, --create-rc create new default rc
-N, --create-rc-to=<file> create new rc to file
NOTE: pid arguments are not allowed with -n, -N
-d, --device show the device format
-q, --quiet do not display header and footer
-p, --show-path show path in the mapping
-A, --range=<low>[,<high>] limit results to the given range

-h, --help display this help and exit
-V, --version output version information and exit

For more details see pmap(1).

@ILUGA:~$ pmap 9\
>
9: -bash
000055da08417000 180K r---- bash
000055da08444000 708K r-x-- bash
000055da084f5000 220K r---- bash
000055da0852c000 16K r---- bash
000055da08530000 36K rw--- bash
000055da08539000 40K rw--- [ anon ]
000055da094d4000 4640K rw--- [ anon ]
00007f7be682f000 12K r---- libnss_files-2.31.so
00007f7be6832000 28K r-x-- libnss_files-2.31.so
00007f7be6839000 8K r---- libnss_files-2.31.so
00007f7be683b000 4K r---- libnss_files-2.31.so
00007f7be683c000 4K rw--- libnss_files-2.31.so
00007f7be683d000 24K rw--- [ anon ]
00007f7be684b000 200K r---- LC_CTYPE
00007f7be687d000 4K r---- LC_NUMERIC
00007f7be687e000 4K r---- LC_TIME
00007f7be687f000 1484K r---- LC_COLLATE
00007f7be69f2000 4K r---- LC_MONETARY
00007f7be69f3000 4K r---- SYS_LC_MESSAGES
00007f7be69f4000 4K r---- LC_PAPER
00007f7be69f5000 4K r---- LC_NAME
00007f7be69f6000 4K r---- LC_ADDRESS
00007f7be69f7000 4K r---- LC_TELEPHONE
00007f7be69f8000 2968K r---- locale-archive
00007f7be6cde000 12K rw--- [ anon ]
00007f7be6ce1000 148K r---- libc-2.31.so
00007f7be6d06000 1504K r-x-- libc-2.31.so
00007f7be6e7e000 296K r---- libc-2.31.so
00007f7be6ec8000 4K ----- libc-2.31.so
00007f7be6ec9000 12K r---- libc-2.31.so
00007f7be6ecc000 12K rw--- libc-2.31.so
00007f7be6ecf000 16K rw--- [ anon ]
00007f7be6ed3000 4K r---- libdl-2.31.so
00007f7be6ed4000 8K r-x-- libdl-2.31.so
00007f7be6ed6000 4K r---- libdl-2.31.so
00007f7be6ed7000 4K r---- libdl-2.31.so
00007f7be6ed8000 4K rw--- libdl-2.31.so
00007f7be6ed9000 56K r---- libtinfo.so.6.2
00007f7be6ee7000 60K r-x-- libtinfo.so.6.2
00007f7be6ef6000 56K r---- libtinfo.so.6.2
00007f7be6f04000 16K r---- libtinfo.so.6.2
00007f7be6f08000 4K rw--- libtinfo.so.6.2
00007f7be6f09000 8K rw--- [ anon ]
00007f7be6f0b000 4K r---- LC_MEASUREMENT
00007f7be6f0c000 28K r--s- gconv-modules.cache
00007f7be6f13000 4K r---- ld-2.31.so
00007f7be6f14000 140K r-x-- ld-2.31.so
00007f7be6f37000 32K r---- ld-2.31.so
00007f7be6f3f000 4K r---- LC_IDENTIFICATION
00007f7be6f40000 4K r---- ld-2.31.so
00007f7be6f41000 4K rw--- ld-2.31.so
00007f7be6f42000 4K rw--- [ anon ]
00007ffc278c0000 132K rw--- [ stack ]
00007ffc2790f000 16K r---- [ anon ]
00007ffc27913000 4K r-x-- [ anon ]
total 13208K

ממליץ בחום ללמוד עוד על מערכות הפעלה ואיך הזיכרון עובד.
זה מעניק את היכולת והכלים להבנה עמוקה יותר של התוכנה וכתיבת קוד ברמה גבוה יותר.

תודה על הקריאה!

על הפוסט

הפוסט נכתב על ידי Ilya, רישיון על ידי CC BY-NC-ND 4.0.

שתפו את הפוסט

Email Facebook Linkedin Print

קנו לי קפה