לדלג לתוכן

מחסניות ומקומיות זיכרון

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

במקביל, שני buffers בגודל זהה יכולים להציג ביצועים שונים בגלל מיקום הזיכרון ביחס למעבד או גודל הדפים. בשיעור הזה נחבר stack, NUMA ו־large pages למודל ההקצאה בלי להפוך אותם לפתרונות קסם.

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

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

#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#include <cstdio>

__declspec(noinline)
unsigned visit(unsigned depth) {
    volatile unsigned char buffer[1024]{};
    buffer[0] = static_cast<unsigned char>(depth);
    if (depth == 0) return buffer[0];
    const unsigned child = visit(depth - 1);
    return child + buffer[0];
}

int main() {
    ULONG_PTR low = 0;
    ULONG_PTR high = 0;
    GetCurrentThreadStackLimits(&low, &high);
    int local = 0;
    std::printf("low=%p high=%p local=%p sum=%u\n",
        reinterpret_cast<void*>(low), reinterpret_cast<void*>(high),
        static_cast<void*>(&local), visit(16));
}
cl /nologo /std:c++20 /EHsc /W4 /Od /Zi stack_lab.cpp /Festack_lab.exe

ה־volatile והפעולה שאחרי הקריאה הרקורסיבית מקלים על שמירת frames בניסוי. עדיין צריך לבדוק את האסמבלי של הכלי שנבחר, ולא להסיק שכל frame צורך בדיוק 1024 בתים. קיימים alignment, כתובת חזרה, שמירות רגיסטרים ומידע נוסף.

הגבולות שחוזרים מה־API הם גבולות stack לפי חוזה Windows. כדי להבחין בין reserved ו־committed משתמשים גם ב־VirtualQuery ובדיבאגר. אין לקרוא לכל ההפרש בין low ל־high זיכרון RAM תושב.

גדילה דורשת מגע מדורג

המערכת משתמשת במנגנוני guard ובחשבונאות כדי להרחיב commitment של stack לפי צורך. קוד מהדר כולל מנגנוני probing עבור הקצאות מחסנית גדולות, כדי לא לדלג על אזור שצריך להפעיל גדילה.

ניסיון לכתוב אסמבלי שמזיז את RSP רחוק מאוד ואז נוגע רק בקצה יכול להפר את הנחות המנגנון. לכן קוד ידני צריך לכבד ABI, probing ו־unwind information יחד. הצלחה של פונקציה קטנה אינה הוכחה שתעבוד עם frame גדול.

גלישת מחסנית יכולה לנבוע מרקורסיה לא מוגבלת, frame גדול או תקציב stack קטן. הגדלת reservation יכולה לדחות באג רקורסיה בלי לתקן אותו. מתחילים משרשרת הקריאות ומגודל frames, ואז בוחרים תיקון.

מקומיות פיזית - NUMA

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

תהליך שמקצה buffer ב־thread אחד ואז מעבד אותו ב־thread על node אחר עלול לשלם על locality לא מתאימה. מנגד, הצמדה קשיחה של כל העבודה ל־node אחד יכולה להגביל איזון עומס. צריך למדוד את שתי ההשפעות.

אפשר לשאול באיזה node נמצא המעבד שנדגם ברגע מסוים. הקטע הבא מצטרף לתוכנית עם Windows headers.

PROCESSOR_NUMBER processor{};
GetCurrentProcessorNumberEx(&processor);
USHORT node = 0;
if (GetNumaProcessorNodeEx(&processor, &node))
    std::printf("group=%u cpu=%u node=%u\n",
        static_cast<unsigned>(processor.Group),
        static_cast<unsigned>(processor.Number),
        static_cast<unsigned>(node));

ה־thread יכול לעבור למעבד אחר אחרי הדגימה. לכן הפלט אינו מדיניות קבועה, אלא תצפית. ניסוי locality רציני מתעד affinity או CPU sets, מיקום הדפים, גודל העבודה ונתיבי הגישה.

בקשת node אינה חוזה של בעלות פיזית נצחית

ממשקים כמו VirtualAllocExNuma מאפשרים להביע העדפה לפי חוזה הפלטפורמה. ההצלחה אינה אומרת שכל דף יישאר לנצח באותו מקום בכל לחץ ותצורה. בודקים תושבות ו־node דרך APIs מתאימים ובוחנים את ההשפעה על ביצועים.

אם במחשב יש node אחד בלבד, אין תוצאה מרובת־nodes למדוד. אפשר ללמוד את החשבון ולנתח trace ממערכת מתאימה, אך אין להמציא פער locality מתוך רעש תזמון.

דפים גדולים ויחידת התרגום

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

הניסוי הבא הוא בקשה מוגבלת מתוך תוכנית מעבדה. הוא אינו משנה privileges; אם ההרשאה המתאימה אינה קיימת, מדווחים על כשל ולא משנים הגדרות מערכת אוטומטית.

const SIZE_T large = GetLargePageMinimum();
if (large != 0 && large <= 64 * 1024 * 1024) {
    void* memory = VirtualAlloc(nullptr, large,
        MEM_RESERVE | MEM_COMMIT | MEM_LARGE_PAGES, PAGE_READWRITE);
    if (!memory)
        std::printf("large page allocation error=%lu\n", GetLastError());
    else
        VirtualFree(memory, 0, MEM_RELEASE);
}

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

משנים משתנה אחד ומודדים

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

הבחירה הסופית נשענת על throughput, latency, peak memory וזמן הקמה. תכונה מתקדמת אינה חובה לכל buffer; היא כלי שמשרת עומס מוגדר.

לקריאה נוספת: גודל וגדילת מחסנית, תמיכת NUMA ותמיכה בדפים גדולים.