विषय पर बढ़ें

अंतिम अद्यतन:2026-10-12

C++17 PMR मेमोरी रिसोर्सेज़

1. आरंभ

C++17 से पहले स्टैंडर्ड लाइब्रेरी std::allocator प्रदान करती थी, जबकि C++17 में इस क्षमता को सुदृढ़ किया गया और polymorphic_allocator पेश किया गया।

C++17 से पहले, यदि हम मेमोरी पूल कस्टमाइज़ करने के लिए std::allocator का उपयोग करना चाहते थे, तो हम पारंपरिक वर्चुअल पॉलीमॉर्फ़िक तरीका नहीं अपना सकते थे, क्योंकि std::allocator में वर्चुअल फ़ंक्शन नहीं होते। इसलिए, inheritance का तरीका स्पष्ट रूप से अच्छा विकल्प नहीं था, लेकिन हम इसे क्लास मेंबर की तरह उपयोग कर सकते थे, जैसा नीचे दिखाया गया है:

template <typename Allocator = std::allocator<uint8_t>>
class MemoryPool {
 public:
  explicit STLMemoryPool(const Allocator& alloc) : alloc_(alloc) {}

  Status Allocate(int64_t size, uint8_t** out) {
    try {
      *out = alloc_.allocate(size);
    } catch (std::bad_alloc& e) {
      return Status::OutOfMemory(e.what());
    }
    return Status::OK();
  }

  void Free(uint8_t* buffer, int64_t size) {
    alloc_.deallocate(buffer, size);
  }

 private:
  Allocator alloc_;
};

C++17 से पहले, यदि हम मेमोरी आवंटन/रिलीज़ के समय कुछ print ऑपरेशन या कुछ कस्टम ऑपरेशन करना चाहते थे, तो दो तरीके अपना सकते थे:

  • ग्लोबल new/delete कस्टमाइज़ करना
void* operator new(std::size_t size) {
  void* ptr = malloc(size);
  std::cout << "Allocated: " << size << " bytes at address " << ptr << std::endl;
  return ptr;
}

void operator delete(void* ptr, std::size_t n) noexcept {
  std::cout << "Deallocated: " << n << " bytes at address " << ptr << std::endl;
  free(ptr);
}
  • allocator कस्टमाइज़ करना
class allocator {
 public:
  using value_type = T;
  value_type* allocate(std::size_t n) {
    value_type* p = static_cast<value_type*>(::operator new(n * sizeof(value_type)));
    std::cout << "Allocated: " << n * sizeof(value_type) << " bytes at address "
              << static_cast<void*>(p) << std::endl;
    return p;
  }

  void deallocate(value_type* p, std::size_t n) noexcept {
    std::cout << "Deallocated: " << n * sizeof(value_type) << " bytes at address "
              << static_cast<void*>(p) << std::endl;
    ::operator delete(p);
  }
};

std::vector<int, allocator<int>> vec;

तो, इनके अलावा और कौन से तरीके हैं?

C++17 के बाद, हम polymorphic memory resources (pmr) नामक बहुरूपी मेमोरी प्रबंधन टूल का उपयोग कर सकते हैं, उपयोग का तरीका:

#include <memory_resources>

pmr::monotonic_buffer_resource res(10);
pmr::vector<vector<int>> vec1(&res);

ऊपर वाली समस्या के लिए, हम memory_resource को inherit कर सकते हैं, नीचे दिए कोड की व्याख्या नीचे दिए गए memory_resource सेक्शन में है।

class TrackAllocator : public std::pmr::memory_resource {
    void* do_allocate(std::size_t bytes, std::size_t alignment) override {
        void* p = std::pmr::new_delete_resource()->allocate(bytes, alignment);
        std::cout << "do_allocate: " << bytes << " bytes at " << p
                  << " alignment: " << alignment << std::endl;
        return p;
    }

    void do_deallocate(void* p, std::size_t bytes, std::size_t alignment) override {
      std::cout << "do_deallocate: " << bytes << " bytes at "
                << " alignment: " << alignment << std::endl;
      return std::pmr::new_delete_resource()->deallocate(p, bytes, alignment);
    }

    bool do_is_equal(const std::pmr::memory_resource& other) const noexcept override {
        return std::pmr::new_delete_resource()->is_equal(other);
    }
};

इस प्रकार, हमने ऊपर वाला प्रभाव हासिल कर लिया।

2.memory_resource

STL में std::par::memory_resource क्लास का क्रियान्वयन काफ़ी सरल है, निम्नलिखित एक pseudo-code क्रियान्वयन है:

class memory_resource {
  static const size_t __max_align = alignof(max_align_t);

public:
  virtual ~memory_resource();

  void* allocate(size_t __bytes, size_t __align = __max_align) {
    return do_allocate(__bytes, __align);
  }

  void deallocate(void* __p, size_t __bytes, size_t __align = __max_align) {
    do_deallocate(__p, __bytes, __align);
  }

  bool is_equal(const memory_resource& __other) const noexcept { 
    return do_is_equal(__other); 
  }

private:
  virtual void* do_allocate(size_t, size_t)                       = 0;
  virtual void do_deallocate(void*, size_t, size_t)               = 0;
  virtual bool do_is_equal(memory_resource const&) const noexcept = 0;
};

डिफ़ॉल्ट std::allocator के विपरीत, memory_resource बहुरूपिता (polymorphism) की क्षमता देता है, जो उपयोगकर्ता को inheritance के ज़रिए निम्न तीन इंटरफ़ेस दोबारा लिखने की अनुमति देता है:

  • do_allocate
  • निर्दिष्ट आकार और alignment मांग वाली मेमोरी आवंटित करने के लिए।
  • do_deallocate
  • पहले do_allocate से आवंटित मेमोरी को रिलीज़ करना।
  • do_is_equal
  • जाँचना कि वर्तमान मेमोरी रिसोर्स ऑब्जेक्ट दूसरे मेमोरी रिसोर्स ऑब्जेक्ट के बराबर है या नहीं।

इसलिए, हमारा ऊपर वाला TrackAllocator इन तीनों फ़ंक्शन इंटरफ़ेस को दोबारा लिखकर काम कर जाएगा।

ध्यान देने वाले मित्र देख गए होंगे कि इन इंटरफ़ेसों के अलावा, memory_resource बाहर की ओर भी तीन इंटरफ़ेस देता है। इसके अलावा, ग्लोबल इंटरफ़ेस भी देता है:

memory_resource* new_delete_resource() noexcept;
memory_resource* null_memory_resource() noexcept;
memory_resource* get_default_resource() noexcept;
memory_resource* set_default_resource(memory_resource* __r) noexcept;
  • new_delete_resource

एक memory_resource पॉइंटर लौटाता है, जो new और delete ऑपरेटरों से मेमोरी आवंटित और रिलीज़ करता है। यह रिसोर्स ग्लोबल new और delete ऑपरेटरों से बना है, इसलिए यह डिफ़ॉल्ट मेमोरी रिसोर्स मैनेजर है। यह memory_resource सबक्लास __resource_adaptor_imp है, जो ऊपर वाले तीनों इंटरफ़ेस दोबारा लिखने का काम करता है।

  • null_memory_resource

एक memory_resource पॉइंटर लौटाता है, जो कुछ न करने वाले खाली मेमोरी रिसोर्स को दर्शाता है। जब आप बिना वास्तविक मेमोरी आवंटन के परीक्षण या प्लेसहोल्डिंग करना चाहते हैं, तो इस रिसोर्स का उपयोग कर सकते हैं। STL सोर्स कोड के क्रियान्वयन में एक

inline memory_resource*
null_memory_resource() noexcept {
    class type final : public memory_resource {};
}

type को सबक्लास के रूप में परिभाषित कर ऊपर वाले तीनों इंटरफ़ेस दोबारा लिखे गए हैं, do_allocate इंटरफ़ेस आवंटन त्रुटि का exception फेंकता है, क्योंकि इसका semantics ही है कि आवंटन की अनुमति नहीं है।

do_allocate(size_t, size_t) override
    { std::__throw_bad_alloc(); }

इसलिए जब हम null_memory_resource का उपयोग करते हैं, तो try catch करना पड़ता है, उदाहरण के लिए: string के कैरेक्टर 16 से कम होने पर आवंटन stack पर होता है, तब हमारे यहाँ के memory_resource का उपयोग नहीं होता, इसलिए सामान्य चलता है, जब लंबाई 16 या उससे अधिक हो, तो std::bad_alloc exception फेंका जाएगा।

std::pmr::memory_resource* default_resource = std::pmr::null_memory_resource();

std::pmr::set_default_resource(default_resource);

try {
  std::pmr::vector<int> data{1};
} catch (std::bad_alloc e) {
  std::cerr << "bad_alloc is thrown vector" << std::endl;
}

std::pmr::string str1{"hello world hel"};
std::cerr << str1 << " length:" << str1.size() << std::endl;

try {
  std::pmr::string str1{"hello world hel0"};
} catch (std::bad_alloc e) {
  std::cerr << "bad_alloc is thrown string" << std::endl;
}
  • get_default_resource

वर्तमान डिफ़ॉल्ट ग्लोबल मेमोरी रिसोर्स मैनेजर लौटाता है। डिफ़ॉल्ट रूप से, यह new_delete_resource() द्वारा लौटाए गए समान रिसोर्स है, लेकिन set_default_resource फ़ंक्शन से डिफ़ॉल्ट रिसोर्स बदला जा सकता है।

  • set_default_resource

मेमोरी रिसोर्स मैनेजर सेट करता है, पैरामीटर खाली हो सकता है (डिफ़ॉल्ट रिसोर्स लौटाता है), अन्यथा दिए गए रिसोर्स पर सेट करता है और पहले का डिफ़ॉल्ट रिसोर्स लौटाता है।

3. मेमोरी पुनः उपयोग

std::pmr::monotonic_buffer_resource एक बफ़र है, जो मेमोरी पुनः उपयोग के लिहाज़ से बड़ा फ़ायदेमंद है। monotonic_buffer_resource, memory_resource को inherit करता है, ऊपर वाले तीनों इंटरफ़ेस दोबारा लिखता है, और आंतरिक रूप से upstream memory_resource रखता है, यदि वर्तमान मेमोरी पर्याप्त न हो, तो upstream से लेता है। इससे मेमोरी fragmentation कम करने और मेमोरी उपयोग दर बढ़ाने में मदद मिलती है।

नीचे std::pmr::monotonic के उपयोग की कुछ विधियाँ दी गई हैं।

विधि 1 - निश्चित बफ़र से monotonic_buffer_resource बनाना:

char buf1[30] = {};
std::fill_n(std::begin(buf1), std::size(buf1) - 1, '_');
std::pmr::monotonic_buffer_resource pool1{std::data(buf1), std::size(buf1)};
std::pmr::vector<int> myVec1{&pool1};

विधि 2 - upstream रिसोर्स का लाभ उठाकर monotonic_buffer_resource बनाना:

std::pmr::memory_resource* upstreamResource2 = std::pmr::get_default_resource();
std::pmr::monotonic_buffer_resource pool2(upstreamResource2);
{
    std::pmr::vector<double> numbers(&pool2);
    for (double d = 0.1; d < 1.0; d += 0.1) {
        numbers.push_back(d);
    }
}

विधि 3 - std::pmr::monotonic_buffer_resource को inherit करना:

class MyMonotonicBufferResource : public std::pmr::monotonic_buffer_resource {
public:
    using std::pmr::monotonic_buffer_resource::monotonic_buffer_resource; // Inherit constructors

    // Public function to access do_allocate
    void* allocate(std::size_t bytes, std::size_t alignment) {
        return do_allocate(bytes, alignment);
    }

    // Public function to access do_deallocate
    void deallocate(void* p, std::size_t bytes, std::size_t alignment) {
        do_deallocate(p, bytes, alignment);
    }
};

MyMonotonicBufferResource pool3;
// Allocate memory
void* mem = pool3.allocate(100, alignof(int));

// Use the allocated memory (this is just an example)
std::pmr::vector<int> numbers({1, 2, 3}, &pool3);

// Deallocate memory
pool3.deallocate(mem, 100, alignof(int));

std::cout << "Usage 3 - Inherit: \n\n";

4.pool resource

synchronized_pool_resource एक थ्रेड-सुरक्षित मेमोरी पूल रिसोर्स क्लास है। इसका डिज़ाइन उद्देश्य मल्टी-थ्रेड वातावरण में सुरक्षित मेमोरी आवंटन और रिलीज़ करना है। जब कई थ्रेड एक साथ मेमोरी आवंटन या रिलीज़ करने का प्रयास करते हैं, तो synchronized_pool_resource synchronization तंत्र से थ्रेड सुरक्षा सुनिश्चित करता है। इससे प्रायः कुछ परफ़ॉर्मेंस ओवरहेड आता है, क्योंकि साझा मेमोरी पूल की रक्षा के लिए लॉक करना पड़ता है।

इसके सम्मुख है unsynchronized_pool_resource, जिसमें उपयोगकर्ता को खुद थ्रेड सुरक्षा सुनिश्चित करनी पड़ती है।

synchronized_pool_resource का सोर्स क्रियान्वयन असल में mutex + unsynchronized_pool_resource ही है।

class synchronized_pool_resource : public memory_resource {

public:
    // some interface
private:
  mutex __mut_;
  unsynchronized_pool_resource __unsync_;
};

pool_options से इन मेमोरी पूलों को कॉन्फ़िगर किया जा सकता है।

struct pool_options {
  size_t max_blocks_per_chunk        = 0;
  size_t largest_required_pool_block = 0;
};
  • max_blocks_per_chunk

प्रत्येक chunk में अधिकतम कितने ब्लॉक हो सकते हैं इसे दर्शाता है। मेमोरी पूल द्वारा upstream मेमोरी रिसोर्स से एक बार में ली जाने वाली मेमोरी की मात्रा सीमित करने के लिए। यदि 0 पर सेट किया जाए, तो कर्नेल munge_options फ़ंक्शन से pool_options को बदल देगा।

  • largest_required_pool_block

मेमोरी पूल द्वारा संभाले जा सकने वाले अधिकतम ब्लॉक आकार को दर्शाता है।

संबंधित विषय

टिप्पणियाँ