Table of Contents
מדוע תקן זיכרון אל-מיקום פולס קצר בקוד של הישגים גבוהים
כל מתכנת C מסתמך על מגוון רחב של תבניות הקצאה, גודל אובייקטים ו-FLT:1 לניהול זיכרון דינמי.פונקציות אלה הן מטרות כלליות, נועדו לעבוד על מגוון רחב של דפוסי הקצאה, גדלים אובייקטים, ותקופות חיים.תחת הישות, הם מנהלים heap, לשמור רשימות חינם, פחם בלוקים חופשיים סמוכים, ולטפל בגמישות זו מגיעה בעלות: הקצאה ורווחה עשויים לדרוש מנעולים (עמודי נייר), עבור רכיבי אחסון קבועים של ציוד אחסון, עבור כל אחד, עיבוד נתונים סטנדרטיים, ומוצרים סטנדרטיים, ומוצרים סטנדרטיים, עבור כל אחד, עיבוד של ציוד אחסון, ופריטים סטנדרטיים של ציוד אחסון, ופריטים סטנדרטיים של ציוד אחסון, עבור כל אחד, עבור כל אחד, עבור כל אחד, עיבוד נתונים סטנדרטיים של ציוד אחסון, ויישומים של ציוד אחסון, וקישורים של ציוד אחסון מאובטחים של ציוד אחסון, וקישורים של ציוד אחסון מאובטחים של ציוד אחסון מאובטחים של ציוד אחסון מאובטחים, ועיבוד נתונים סטנדרטיים, ועיבוד נתונים סטנדרטיים של כל אחד, ויישומים של כל אחד, עבור כל אחד, ועיבוד יעיל של ציוד אחסון מאובטחים, ויישומים של ציוד אחסון מאובטח, וקישורים של כל אחד, ועיבוד יעיל של כל אחד
מעבר למהירות הגלם, פיצול הוא רוצח ביצועים שקטים.לאורך זמן, פיזור:2 יכול לפזר הקצאות קטנות על פני הערימה, להשאיר פערים שלא ניתן להשתמש בהם ביעילות.זה מוביל לשימוש זיכרון מוגבר, הקצאות עתידיות איטיות, ומחזורי זיכרון בזבזניים.מאגר זיכרון המכס כוללים חלופה מזערית, נמוכה מראש על ידי הקצאת אזורים גדולים והפעלה קבועה של בלוקים פשוטים של מערכת אחסון, או חסימתית, כמו חלקיקים של גודל גוף פתוח, או חלקיקים, כלומר, כלומר, או חלקיקים מצוינים, או חלקיקים, או חלקיקים, כלומר, כלומר, כלומר, תכונות מהירות גבוהה יותר, חלקיקים של חלקיקים של חיזוי, כלומר, או חלקיקים של גודל של גודל של גודל של גודל של חלקיקים של חלקיקים של גודל של גודל של חלקיקים של חלקיקים של חלקיקים של חלקיקים של חלקיקים, עם גודל של חלקיקים, או חלקיקים, נמוך יותר, כלומר, נמוך יותר גבוה, עם גודל של חלקיקים, כלומר, עם גודל של חלקיקים, חלקיקים של חלקיקים של חלקיקים, חלקיקים של חלקיקים, כלומר,
מדריך זה הולך לך באמצעות תכנון ויישום מאגר זיכרון בגודל קבוע C. תלמד כיצד לבנות את הבריכה, להתמודד עם מקרים כמו תשישות והיערכות, ולהרחיב את התבנית לתרחישים רב-פול.בסוף, יהיה לך כלי המספק פעולות זיכרון לטווח קצר-קו-נט-זמן והתאמה חלקה לתוך צינורות ביצועים גבוהים.
עקרונות עיצוב הליבה של בריכת זיכרון
בריכת זיכרון (נקראת גם סלקטור או בריכת אובייקטים) פועלת על רעיון פשוט: להקצות בלוק גדול של זיכרון, לחלק אותו ל"הרבה" קבוע, ולנהל אילו חריצים חופשיים באמצעות רשימה מלוכדת לשיר.כאשר זיכרון מבקש הצרכן, הבריכה מחזירה את החריץ הראשון מן הרשימה החופשית.
המונחים: differentable-Size Pools
הגרסה הנפוצה ביותר היא הבריכה בגודל קבוע, שבו כל מרווח הוא אותו גודל.זה תואם את האובייקט שהבילון משרת - לדוגמה, בריכה של FLT 3 צומתים. בריכות בגודל משתנה (נקרא גם "מכלים של אנמנה") יכול להקצות חלקים של גדלים שונים, אבל הם מציגים מורכבות: הם חייבים לנהל רשימה חופשית של גדלים שונים של בלוקים שונים, טיפול פיצולים וחילול פחם, עדיין לא מספיקים, הם עדיין להתמקד במקרים של גודל גבוה, הם.
שיקולים
(ה-CPUs מודרניים דורשים או מעדיפים גישה זיכרון מיושר אם הבריכה שלך מאחסנת פריטים המכילים סוגים כגון FLT:4, FLT:5, או טורי SIMD, הבריכה חייבת להבטיח שכל מרווח מתחיל בכתובת היישר אל הדרישה הגדולה ביותר של הסוג המאוחסן.
המונחים: Safety
עבור יישומים חד-פעמיים, אין צורך בסינכרון.עם זאת, מערכות ייצור רבות דורשות גישה עכשווית.הוספת בטיחות חוט לבריכה היא פשוטה: להגן על הרשימה החינמית עם mutex, או להשתמש ברשימה מקושרת ללא מנעול עם אטומי השווה ו-Swap.We יציג את הגרסה הבסיסית שנקראת יחיד, אבל אנחנו נעסוק נקודות הרחבה עבור סביבות מרובות-A משותף הוא מאגר תוכן משלו.
בניית בריכת זיכרון קבועה: צעד אחר צעד
אנו ניישם מאגר שמאחסן חפצים בגודל שרירותי.הבריכות עצמה היא מבנה המחזיק נקודה לזיכרון שלפני הכל, ראש רשימה חופשית, גודל החריץ (מסביב להיערכות), ומספר מוחלט של חריצים.הרשימה החינמית היא רשימה מקושרת של טבועת FLT:0insideFLT:1 כל מרווח חינם: כל אחד מחנויות חינם נקודה עד קצה הבא לכל מטבול.
מבנה נתונים
#include <stddef.h>
#include <stdlib.h>
// Embedded free list node
typedef struct FreeNode {
struct FreeNode* next;
} FreeNode;
// Pool descriptor
typedef struct MemoryPool {
size_t slot_size; // Size of each slot (after alignment rounding)
size_t slot_count; // Number of slots in the pool
void* pool_start; // Start of the pre‑allocated memory block
FreeNode* free_list; // Head of the free list
} MemoryPool;
(FLT:10) משמש רק פנימי; להקצות חפצים הכובשים את אותו זיכרון.כאשר מזל הוא חינם, המדפים הראשונים שלו מכילים נקודה הבאה.כאשר הוא מוקצה, המשתמש בבריכת כותב את הנתונים שלהם על אותו נקודה.זו הסיבה לכך שגודל החריץ חייב להיות לפחות FLT:11 - אחרת אנחנו לא יכולים לאחסן את המצביעים החופשיים.
תחילת
הסימון מקצה גוש זיכרון גדול וקישורים לכל מרווח לרשימה חופשית.We Round up the Seek size to the most following multiple of היישור (שאנו בוחרים כ-FLT:12).
#include <stdint.h> // for max_align_t
int pool_init(MemoryPool* mp, size_t object_size, size_t object_count) {
// Round up object_size to the alignment of max_align_t
size_t alignment = _Alignof(max_align_t);
size_t aligned_size = (object_size + alignment - 1) & ~(alignment - 1);
// Ensure slot is large enough to hold a FreeNode pointer
if (aligned_size < sizeof(FreeNode))
aligned_size = sizeof(FreeNode);
mp->slot_size = aligned_size;
mp->slot_count = object_count;
// Allocate the contiguous pool memory
size_t total_size = aligned_size * object_count;
mp->pool_start = malloc(total_size);
if (mp->pool_start == NULL)
return -1; // allocation failure
// Build the free list
mp->free_list = (FreeNode*)mp->pool_start;
FreeNode* current = mp->free_list;
for (size_t i = 1; i < object_count; i++) {
current->next = (FreeNode*)((char*)mp->pool_start + i * aligned_size);
current = current->next;
}
current->next = NULL;
return 0;
}
אנו משתמשים ב-FLT:14 אשר נותן את ערבות ההשתנות הקפדנית הנדרשת על ידי (FLT:15 עבור רוב הפלטפורמות זה 8 או 16 ע"י חתכים.הטריק המסובך ביותר פועל עבור כוח-of-Two היערכות זו מבטיחה שכל אחד מהם חוזר הוא בטוח לשימוש בכל סוג סטנדרטי.
אל-מיקום
אללוקו מפצח בראש הרשימה החופשית וחוזרת אליה אם הרשימה החינמית ריקה, הבריכה מותשת ואנחנו חוזרים ל-FLT:16.
void* pool_alloc(MemoryPool* mp) {
if (mp->free_list == NULL) {
return NULL; // pool exhausted
}
FreeNode* block = mp->free_list;
mp->free_list = block->next;
return (void*)block;
}
זהו O(1) ומבצע מספר הוראות.לא מנעולים, שום מערכת לא קוראת.
לשחרר את ה-Slet
שחרור דוחף את החריץ בחזרה לרשימה החינמית.הטלפון חייב להבטיח שהמציין שייך לבריכת זו (נדון בהמשך אימות).
void pool_free(MemoryPool* mp, void* ptr) {
if (ptr == NULL) return; // standard behavior like free(NULL)
FreeNode* node = (FreeNode*)ptr;
node->next = mp->free_list;
mp->free_list = node;
}
שוב O(1) לא פחם, שום מיזוג.המזל החופשי הופך זמין לשימוש חוזר.
חורבן הבריכה
כאשר הבריכה כבר לא נדרשת, שחררו את ההקצאה הבסיסית.
void pool_destroy(MemoryPool* mp) {
free(mp->pool_start);
mp->pool_start = NULL;
mp->free_list = NULL;
mp->slot_count = 0;
mp->slot_size = 0;
}
תמיד לקרוא ל-FLT:20 לפני שמבנה הבריכה יוצא מההיקף כדי למנוע דליפות זיכרון.
דוגמא
הנה דוגמה שלמה שיוצרת מאגר של 1024 חריצים, מקצה אחד, כותב ערך, קורא אותו, ומשחרר אותו.
#include <stdio.h>
#include <assert.h>
int main(void) {
MemoryPool pool;
if (pool_init(&pool, sizeof(int), 1024) != 0) {
fprintf(stderr, "Pool initialization failed\n");
return 1;
}
int* p = (int*)pool_alloc(&pool);
if (p == NULL) {
fprintf(stderr, "Pool exhausted\n");
return 1;
}
*p = 42;
printf("Value: %d\n", *p);
pool_free(&pool, p);
pool_destroy(&pool);
return 0;
}
בבקשה אמיתית, היית מקצה בריכה לכל סוג של אובייקט שאתה צריך לנהל, למשל, שרת רשת יכול להיות בעל ההרחבה 22 ו-FLT:23.
שיקולים מתקדמים ורחבות
עקבו אחרי Allocation for Debugging
הבריכה הבסיסית אינה עוקבה אילו חריצים מוקצה כיום.עבור פיזור, אתה יכול להוסיף קצתפילד או רשימה נפרדת של בלוקים שהוקצו.זה מאפשר לך לזהות נטולי כפול או דליפות.בייצור, את פני של מעקב הוא בדרך כלל נמנע - האופי הדטרמיניסטי של בריכות הופך באגים לקלים יותר למצוא באמצעות הרעלה זיכרון.
זיכרון הרעלה
כאשר משתחררת מזל, תוכל לנסח את תוכנו עם דפוס ידוע (למשל, FLT:24) כדי לזהות שימוש ללא תשלום, באופן דומה, כאשר הקצאה, תוכל למלא את החריץ עם דפוס כדי לתפוס קריאה ללא עוררין.
יצוא מידע על Pool Statistics
עבור ביצוע כוונון, לחשוף ניגודים כמו הקצאות הכוללות, חינם מוחלט, וספירה חופשית נוכחית.דרך פשוטה היא לשמור על שדה 25FLT במבנה הבריכה, החל על כלוק וההפצה על חינם.
// Add to MemoryPool: size_t free_count;
// In pool_alloc: if (mp->free_list) { mp->free_count--; ... }
// In pool_free: mp->free_count++; ...
תגית:Safe Pools
לקבלת גישה זו, עוטפים את כל הפונקציות החופשיות והכוליות עם mutex:
#include <pthread.h>
typedef struct ThreadSafePool {
MemoryPool pool;
pthread_mutex_t lock;
} ThreadSafePool;
void* ts_pool_alloc(ThreadSafePool* tsp) {
pthread_mutex_lock(&tsp->lock);
void* ptr = pool_alloc(&tsp->pool);
pthread_mutex_unlock(&tsp->lock);
return ptr;
}
void ts_pool_free(ThreadSafePool* tsp, void* ptr) {
pthread_mutex_lock(&tsp->lock);
pool_free(&tsp->pool, ptr);
pthread_mutex_unlock(&tsp->lock);
}
לקבלת תוכן נמוך יותר, שקול רשימה חופשית ללא מנעול באמצעות (FLT:28 עם זאת, הדורש טיפול בבעיה ABA - אתגר קלאסי שתואר בספרי לימוד רבים של מטבעות מסחר.
גדל הבריכה דינמי
בריכות בגודל קבוע לא יכולות לגדול פעם ראשונה.אם אתה צריך בריכה שיכולה להתרחב, אתה יכול לשמור על מערך של נתחי בריכה. כאשר נתח אחד מותש, להקצות חדש (של אותו גודל) ולהוסיף את חריצים שלה לרשימה החינמית.הסופר נשאר O (1) כמעט תמיד, אבל אתה צריך לנהל מספר רב של נתחים במהלך ההרס.
Benchmarks (conceptual)
במיקרו-בקנצ'מרק טיפוסי על x86-64 CPU מודרני, מחזור אלגורית בריכה לוקח 15-30 ננו השניות, בעוד FLT:29 / veFLT:30 עבור אובייקט 32-byte יכול לקחת 80-200 ננו השניות בשל נעילה ו metadata overhead. ביישומים אמיתיים, השיפור הוא לעתים קרובות 2–5× עבור עבודה רב-כבדה יותר, כי הם טובים יותר ויותר אובייקטים מרחביים.
מלכודות נפוצות וכיצד להימנע מהם
- (ב) ⁇ ⁇ ⁇ ⁇ ⁇ ⁇ ⁇ ⁇ ⁇ ⁇ ⁇ ⁇ ⁇ ⁇ ⁇ ⁇ ⁇ ⁇ ⁇ ⁇ ⁇ ⁇ ⁇ ⁇ ⁇ ⁇ ⁇ ⁇ ⁇ ⁇ ⁇ ⁇ ⁇ ⁇ ⁇ ⁇ ⁇ ⁇ ⁇ ⁇ ⁇ ⁇ ⁇ ⁇ ⁇ ⁇ ⁇ ⁇ ⁇ ⁇ ⁇ ⁇ ⁇ ⁇ ⁇ ⁇ ⁇ ⁇ ⁇ ⁇ ⁇ ⁇ ⁇ ⁇ ⁇ ⁇ ⁇ ⁇ ⁇ ⁇ ⁇ ⁇ ⁇ ⁇ ⁇ ⁇ ⁇ ⁇ ⁇ ⁇ ⁇ ⁇ ⁇ ⁇ ⁇ ⁇ ⁇ ⁇ ⁇ ⁇ ⁇ ⁇ ⁇ ⁇ ⁇ ⁇ ⁇ ⁇ ⁇ ⁇ ⁇ ⁇ ⁇ ⁇ ⁇ ⁇ ⁇ ⁇ ⁇ ⁇ ⁇ ⁇ ⁇ ⁇ ⁇ ⁇ ⁇ ⁇ ⁇ ⁇ ⁇ ⁇ ⁇ ⁇ ⁇ ⁇
- (ב) אם אתה מאחסן את סוגי ההיערכות יוצאת דופן (למשל, FLT:33), להבטיח את היערכות ההצלחות שלך מספיק.
- (ב) ,0) , ⁇ (ב) , ⁇ ⁇ ⁇ ⁇ ⁇ ⁇ ⁇ ⁇ ⁇ ⁇ ⁇ ⁇ ⁇ ⁇ ⁇ ⁇ ⁇ ⁇ ⁇ ⁇ ⁇ ⁇ ⁇ ⁇ ⁇ ⁇ ⁇ ⁇ ⁇ ⁇ ⁇ ⁇ ⁇ ⁇ ⁇ ⁇ ⁇ ⁇ ⁇ ⁇ ⁇ ⁇ ⁇ ⁇ ⁇ ⁇ ⁇ ⁇ ⁇ ⁇ ⁇ ⁇ ⁇ ⁇ ⁇ ⁇ ⁇ ⁇ ⁇ ⁇ ⁇ ⁇ ⁇ ⁇ ⁇ ⁇ ⁇ ⁇ ⁇ ⁇ ⁇ ⁇ ⁇ ⁇ ⁇ ⁇ ⁇ ⁇ ⁇ ⁇ ⁇ ⁇ ⁇ ⁇ ⁇ ⁇ ⁇ ⁇ ⁇ ⁇ ⁇ ⁇ ⁇ ⁇ ⁇ ⁇ ⁇ ⁇ ⁇ ⁇ ⁇ ⁇ ⁇ ⁇ ⁇ ⁇ ⁇ ⁇ ⁇ ⁇ ⁇ ⁇ ⁇ ⁇ ⁇ ⁇ ⁇ ⁇ ⁇ ⁇
- (FLT:0) השימוש בבריכה להקצאות בגודל משתנה:FLT 1:1 אם אתה צריך אובייקטים בגדלים שונים, ליצור בריכות נפרדות. מנסה להתאים את הגדלים המשתנים לזיכרון פסולת בגודל קבוע או גורם לטרנקום.
אמת-עולם קונטקסט וקריאה נוספת
בריכות זיכרון מותאמות הן לא רעיון חדש.הם מופיעים כמעט בכל מערכת ביצועים גבוהה:
- לינוקס משתמשת ב-FLT:0 slab AllocatorsFLT 1:1 עבור משככי אובייקטים (ראה ממשק FLT:37).
- מנועי המשחק (FLT:0) Unreal EngineFLT:1ua ו-FLT:2 GodotphFLT 3: 3 מספקים ocators בריכה מובנה עבור שחקנים חלקיקים.
- ספריות רשת (למשל, FLT:0DPDáveFLT) משתמשות במאגרי זיכרון עבור חפיסות, כדי להבטיח אפס הקצאה על הנתיב המהיר.
- ספריית Apache (FLT:0) APRIRFLT 1 כוללת ממשק API של בריכה המשמש את Apache HTTP Server.
(ב) ב[[1924]], [[1924]]]], [[1924]]]]]], [[1924]]]]]], [[1924]]]]]], [[1924]]]]]]]]]]]], [[1924]]]]]]]]]], [[1924]]]]]]]]]]]], [[1924]]]]]]]]]], [[1924]]]]]]]]]]]]]], [[1924]]]]
מסקנה
Allocators של מאגר זיכרון מותאם אישית הוא אופטימיזציה מעשית, גבוהה ליישומים שמנהלים חפצים קטנים, קצרים מועדיים רבים.היישום ב C טהור הוא קטן - נע בין 50 שורות של קוד בעל מבנה - אך הוא מבטל פיצול, מטמון מפספס, ואת ראש של כלנקטורים למטרות כלליות. על ידי הבנת פערי המסחר (גודל מול גודל משתנה, חוט בטיחות), ניתן להתאים את המערכת הספציפית של תפקוד זה, קרוב להגדרה של עבודת הזיכרון שלך.