לדלג לתוכן

קבוצת עבודה ולחץ זיכרון

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

ה־working set מתאר דפים שנמצאים כרגע בזיכרון הפיזי בהקשר הרלוונטי של התהליך. הוא אינו זהה ל־private commit, לסך reservations או לגודל הנתונים שהיישום חושב שהוא מחזיק. כדי לאבחן לחץ זיכרון צריך להשוות מדדים ולא לבחור מספר אחד.

משנים תושבות בלי לשחרר הזמנה

הניסוי הבא מקצה טווח מוגבל, נוגע בו, מבקש לצמצם את working set של התהליך עצמו ואז נוגע בו שוב. אין בו יצירת לחץ בלתי מוגבל על המחשב.

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

bool report(const char* label) {
    PROCESS_MEMORY_COUNTERS_EX counters{};
    counters.cb = sizeof(counters);
    if (!GetProcessMemoryInfo(GetCurrentProcess(),
        reinterpret_cast<PROCESS_MEMORY_COUNTERS*>(&counters),
        sizeof(counters))) return false;
    std::printf("%s working=%zu private=%zu faults=%lu\n", label,
        static_cast<std::size_t>(counters.WorkingSetSize),
        static_cast<std::size_t>(counters.PrivateUsage),
        counters.PageFaultCount);
    return true;
}

int main() {
    SYSTEM_INFO info{};
    GetSystemInfo(&info);
    constexpr SIZE_T size = 16 * 1024 * 1024;
    void* memory = VirtualAlloc(nullptr, size,
        MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE);
    if (!memory) return 1;
    auto* data = static_cast<volatile unsigned char*>(memory);
    report("committed");
    for (SIZE_T offset = 0; offset < size; offset += info.dwPageSize)
        data[offset] = 11;
    report("touched");

    if (!EmptyWorkingSet(GetCurrentProcess()))
        std::printf("trim error=%lu\n", GetLastError());
    report("after trim");
    unsigned long long sum = 0;
    for (SIZE_T offset = 0; offset < size; offset += info.dwPageSize)
        sum += data[offset];
    report("touched again");
    std::printf("sum=%llu\n", sum);
    if (!VirtualFree(memory, 0, MEM_RELEASE)) return 2;
    report("released");
}
cl /nologo /std:c++20 /EHsc /W4 working_set.cpp /Feworking_set.exe /link Psapi.lib

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

הנתונים עדיין שייכים לנו

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

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

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

רשימות דפים הן מצבי ניהול

מנהל הזיכרון מחזיק דפים במצבים שונים, למשל active, standby, modified, free ו־zeroed לפי המימוש. דף שיצא מה־working set יכול להישאר ב־RAM ולהיות זמין לחזרה מהירה. דף ששונה עשוי לדרוש כתיבה לפני שימוש חוזר במסגרת.

המונחים האלה מתארים מצבי מערכת, ולא שמות של מכולות C++ שהיישום מנהל. המעבר ביניהם יכול להתרחש בלי קריאה ישירה של התוכנית. חקירה עמוקה משתמשת בכלי זיכרון וב־kernel symbols, אך קוד יישומי נתמך מתבסס על APIs ומדדים מתועדים.

ההבדל בין standby לבין free חשוב במיוחד: זיכרון ששומר תוכן שימושי כ־cache יכול להיחשב זמין לשימוש חוזר בלי להיות חסר תוכן. גרף שמראה מעט free אינו לבדו הוכחה למחסור.

דחיסה משנה עלות, לא בעלות

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

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

לחץ חיצוני משפיע על תהליך תקין

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

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

בוחנים ארבע אבחנות שונות

בדליפה, בעלות אובדת או נשמרת ללא גבול ביחס לעבודה. ב־cache מכוון, יש מדיניות שימור ומגבלה שאפשר להסביר. ב־fragmentation, אחסון יכול להיות פנוי לוגית אך לא זמין לבקשה בצורה הרצויה. בלחץ residency, נתונים תקינים נשארים בבעלותנו אך הגישה אליהם דורשת יותר עבודה.

כל אבחנה דורשת ראיות אחרות. ספירה של אובייקטים והקצאות בודקת בעלות; מדדי commit בודקים התחייבות; working set ו־faults בודקים תושבות; trace מחבר את כל אלה לזמן.

לקריאה נוספת: מדדי זיכרון של תהליך ובקשת צמצום working set.