September 3, 2022

C++ ריווח ויישור זיכרון באובייקטים

אובייקטים בזיכרון

אובייקטים פשוטים ופרימיטיביים יכולים להיות ממופים לזכרון והפוך.
זה אומר שניתן לקחת זיכרון ופשוט להמיר באותו לאובייקט:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
struct A
{
int Value;
int Value2;
};

int main()
{
int memory[2]{ 1,2 };

A representation = *((A*)&memory);

std::cout << representation.Value << "\n";
std::cout << representation.Value2;

}

גודל משתנים

אי אפשר לדבר על זיכרון ללא אזכור לנושא גודל המשתנים.
זה די נפוץ להניח ש-int הוא 32 סיביות בעוד ש-long הוא 64 סיביות.
רובנו מניחים שגודל המשתנים הוא כך:

  • Shortהוא 16 סיביות.
  • Int הוא 32 סיביות.
  • Long ו-Ptr הם 64 סיביות.

הסטנדרט Data Model הוא קובע מהו גודל המשתנים.
אנחנו נניח שאנחנו משתמשים ב-LP4.
ע”י הרצה מאוד פשוטה של sizeof(type) דרך קוד נוכל לבדוק את גודל המשתנים.
אצלי:

1
2
3
4
5
Short 2
Int 4
Long 4
Long long 8
Ptr 8

יישור של אובייקטים

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

נתון לנו את המבנה הבא:

1
2
3
4
5
struct A
{
int Value;
int Value2;
};

ל-A יש 2 מספרים אשר כל אחד מהם הוא 4 בתים.
לכן היישור של האובייקט הוא 4 בתים.

בעזרת האופרטור alignof נוכל לבדוק זאת בקלות:

1
2
3
4
5
int main()
{
std::cout << "Alignment of A is " << alignof(A) << "\n";
// מדפיס 4.
}

יישור האובייקט A בנוי בצורה מושלמת פה כי ערך היישור הוא 4 בתים.
האובייקט בנוי מ-2 משתנים מסוג int אשר תופסים סך הכל 8 בתים.

מה ייקרה אם נוסיף משתנה נוסף מסוג char?

1
2
3
4
5
6
struct A
{
int Value;
int Value2;
char Value3;
};

כדי לענות על השאלה הזו נוכל לבדוק בעזרת alignof() ו-sizeof():

לפני:

1
2
Alingment of Previous A is 4
Size of Previous A is 8

אחרי:

1
2
Alingment of A is 4
Size of A is 12

מה קרה פה?
אנחנו יודעים ש-char הוא בגודל בית בודד.
איך הגענו מ-8 בתים ל-12 בתים?
זה הכל בגלל תהליך היישור!

הקומפיילר יודע לבצע יישור אוטומטי מבלי לומר לערך המינימלי ביותר שהוא יכול ליישר מבנה לפיו.

יישור קיים על מנת שנוכל למפות אובייקטים בצורה מיטבית בזיכרון:


מה ייקרה אם ננסה להתעלם מיישור?

השורה ראשונה והשנייה כבר לא מיושרות!

למה מיישרים לחזקת 2?

  • ככה המחשב בנוי
    כל ביט במחשב הוא ערך בינארי - 0 או 1.
    סוגי משתנים הם גם כפולות של שתיים.

Byte - 8 בתים,
Short - 16 בתים,
Int - 32 בתים,
Long long - 64 בתים.

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

  • Cache lines ו-Pages.

כדי לבצע אופטימיזציות ביצועים זיכרון המטמון בנוי בצורה כזו שאנחנו צריכים לטעון חזקות 2 של זיכרון.
גודל נפוץ הוא 64 בתים לשורה בזיכרון מטמון.
כך שאם יהיה לנו אובייקט בגודל 68 בתים - נצטרך לטעון 2 שורות במקום 1.

ניקח את המבנה האחרון כדוגמא - אם נעבור על רשימת אובייקטים בלולאה, איך הזיכרון ייצטרך להיטען?

1
2
3
4
5
6
for(auto& AObj : listOfAs)
{
std::cout << AObj.Value << "\n"; // הזיכרון בשורה
std::cout << AObj.Value2 << "\n"; // לא כל הזיכרון הוא בשורה
std::cout << AObj.Value3 << "\n"; // ערך הזיכרון הזה כבר לא בשורה
}

ריווח

רווחים באובייקטים קיימים כדי להשלים יישור.

דוגמא א

1
2
3
4
5
6
7
struct A
{
int Value; // 4 bytes
int Value2; // 4 bytes
char Value3; // 1 bytes
/* Padding of 3*/ // 3 bytes
};

מבנה A בגודל 12 בתים והוא מיושר ב-4.

דוגמא ב

1
2
3
4
5
6
7
struct A
{
int Value; // 4 bytes
char Value3; // 1 bytes
/* Padding of 3*/ // 3 bytes
int Value2; // 4 bytes
};

מה ייקרה כאשר נזיז את char Value3 לשורה העליונה?
האם זה משפיע על המבנה?
כן ולא.

הריווח קיים בתוך המבנה ולא בסופו אבל הוא עדיין קיים.

דוגמא ג

1
2
3
4
5
6
7
struct A
{
int Value;
char Value2;
long long Value3;
int Value4;
};

גודל המבנה כעת הוא 24 בתים!
איך הוא בנוי?

Value הוא בגודל 4 בתים.
Value2 בגודל בית ויש לו עוד 3 בתים לריווח.
Value3 הוא בגודל 8 בתים, Value ו-Value2 שניהם מיושרים ל-8.
Value4 הוא 4 בתים מכיוון שהיישור ל-8 אנחנו מוסיפים 4 בתים לריווח.

1
2
3
4
5
6
int 4 bytes
char 1 byte
3 bytes padding
long long 8 bytes
int 4 bytes
4 bytes padding

4 + 1 + 3 + 8 + 4 + 4 = 24

מהו המינימום ליישור כעת?

alingas

על מנת לבקש ריווח שונה נוכל להשתמש ב-alignas.
שימו לב שאי אפשר לבקש יישור שונה מהמינימום שהקומפיילר מזהה.

1
2
3
4
5
6
7
struct alignas(16) A
{
int Value;
char Value2;
long long Value3;
int Value4;
};

מבנה A מיושר ל-16 וגודלו הכולל הוא 32.

למה אנחנו צריכים יישור שונה?
הסיבה הבסיסית ביותר היא - ביצועים.

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

ניתן לראות זאת בדוגמא הקודמת:

אם שורה בזיכרון המטמון שלנו הוא 24 בתים - כדי להביא את האובייקט השלישי נצטרך לבצע טעינה של 2 שורות.
על מנת שנוכל ליישר ל24 נצטרך 2 אובייקטים - כל גודל אובייקט הוא 18 בתים ויש לנו ריווח כולל של 6 בתים.

אילו בעיות עולות מחורים בזיכרון?

  • בעיות פרגמנטציה שיביאו לשימוש גדול יותר של זיכרון.
    עבור 2 אובייקטים נאבד מהשימוש 6 בתים.
    ב-100 אובייקטים נאבד 600 בתים!

בקבצים גדולים נוכל להחזיק לעיתים גם עשרות אלפי אובייקטים.
ב10,000 אובייקטים נאבד 58 קילובייטים!
זה מלא.


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

לא מסודר:

1
2
3
4
5
6
7
8
struct A
{
int Value;
bool Value2;
int Value3;
char Value4;
short Value5;
};
1
2
Alingment of A is 4
Size of A is 16

מסודר:

1
2
3
4
5
6
7
8
struct A
{
int Value;
bool Value2;
int Value3;
char Value4;
short Value5;
};
1
2
Alingment of A is 4
Size of A is 12

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

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

על הפוסט

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

שתפו את הפוסט

Email Facebook Linkedin Print

קנו לי קפה

#Software#CPP#Alignment