לדלג לתוכן

2.8 - מקצים ומשאבי זיכרון - פתרון

פתרון - המונה משתנה רק אחרי הצלחה

ב־CountingResource מן ההרצאה מוסיפים שדה ציבורי:

std::size_t remaining = static_cast<std::size_t>(-1);

מחליפים את do_allocate במימוש הבא ומכלילים גם את header של new:

#include <new>

void* do_allocate(std::size_t bytes, std::size_t alignment) override {
    if (remaining == 0) throw std::bad_alloc{};
    void* result = upstream_->allocate(bytes, alignment);
    --remaining;
    live_bytes += bytes;
    ++allocations;
    return result;
}

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

ה־main הבא מחליף את main המקורי:

int main() {
    for (std::size_t limit = 0; limit < 12; ++limit) {
        CountingResource resource;
        resource.remaining = limit;

        try {
            std::pmr::vector<int> values{&resource};
            for (int i = 0; i < 100; ++i) values.push_back(i);
        } catch (const std::bad_alloc&) {
        }

        assert(resource.live_bytes == 0);
    }
}

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

למה reserve משנה את הניסוי

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

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

שוויון אינו רק מקור גיבוי

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

ה־resource אינה מוחזקת בבעלות ה־vector. לכן מציבים אותה בscope חיצוני, ומסיימים את חיי כל המכולות לפני השמדתה או לפני איפוס אחסון שלהן. זאת אותה הוכחת lifetime שלמדנו עבור span, רק שהפעם ה־alias מצביע למנגנון הקצאה.