探索C++17中的pmr模块


orchid
电梯物联网专家 2024-01-19 15:48:38 62095 赞同 0 反对 0
分类: 资源 标签: 后端
探索C++17中的pmr模块

1.引入

在C++17之前,标准库提供了std::allocator,而在C++17中,这一功能得到了加强,引入了polymorphic_allocator

注:本节所有的源码戳文末~

在C++17之前,如果我们想要使用std::allocator来自定义内存池,我们不能使用传统的虚拟多态方式,因为std::allocator并没有提供虚拟函数。因此,通过继承显然不是一个好的选择,但是我们可以将其作为类成员使用,如下所示:

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,有关下面代码的解释见下方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类的实现比较简单,以下为一个伪代码实现:

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提供了多态的能力,允许用户通过继承的方式,重写以下三个接口:

  • do_allocate
    • 用于分配指定大小和对齐要求的内存。
  • 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指针,该指针使用newdelete运算符来分配和释放内存。这个资源是使用全局的newdelete运算符实现的,因此它是默认的内存资源管理器。这个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接口会跑出分配错误的异常,因为语义就是不允许分配了。

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异常。

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,重写上面三个接口,内部会持有上游的memory_resource,如果当前内存不足,便会从上游去取。这有助于降低内存碎片,提高内存利用率。

下面列出一些有关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 - 利用上游资源创建 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:

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 使用同步机制确保线程安全性。这通常会带来一些性能开销,因为需要加锁来保护共享的内存池。

与之对应的是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

表示每个块中最多可以包含的块数量。用于限制内存池从上游内存资源一次性获取的内存量。如果设置为 0,内核会通过munge_options函数去修改pool_options。

  • largest_required_pool_block

表示内存池能够处理的最大块大小。

如果您发现该资源为电子书等存在侵权的资源或对该资源描述不正确等,可点击“私信”按钮向作者进行反馈;如作者无回复可进行平台仲裁,我们会在第一时间进行处理!

评价 0 条
电梯物联网专家L2
粉丝 1 资源 185 + 关注 私信
最近热门资源
分享如何在银河麒麟高级服务器操作系统V10SP3中需要启用内核审计功能。  176
分享免费开源高速下载器  171
一图对比分析IPv4与IPv6  167
统信uos家庭版与专业版的选择  166
分享如何查看网卡中断的数量  163
解决银河麒麟无法添加惠普打印机,提示'client-error-not-possible'.”错误  162
winrar绿色无广告版分享  154
通过shell脚本在统信UOS/麒麟系统中安装nginx  145
分享在麒麟系统中关闭占用端口的进程的办法  142
统信UOS常见问题小总结  142
最近下载排行榜
分享如何在银河麒麟高级服务器操作系统V10SP3中需要启用内核审计功能。 0
分享免费开源高速下载器 0
一图对比分析IPv4与IPv6 0
统信uos家庭版与专业版的选择 0
分享如何查看网卡中断的数量 0
解决银河麒麟无法添加惠普打印机,提示'client-error-not-possible'.”错误 0
winrar绿色无广告版分享 0
通过shell脚本在统信UOS/麒麟系统中安装nginx 0
分享在麒麟系统中关闭占用端口的进程的办法 0
统信UOS常见问题小总结 0
作者收入月榜
1

prtyaa 收益395.97元

2

zlj141319 收益228.47元

3

IT-feng 收益214.92元

4

1843880570 收益214.2元

5

风晓 收益208.24元

6

777 收益173.02元

7

哆啦漫漫喵 收益131.6元

8

Fhawking 收益106.6元

9

信创来了 收益105.97元

10

克里斯蒂亚诺诺 收益91.08元

请使用微信扫码

加入交流群

请使用微信扫一扫!