多态内存资源


多态内存资源

文章说得很详细(虽然我还是不怎么懂,做个笔记吧)
https://github.com/MeouSker77/Cpp17/blob/master/markdown/src/ch29.md

多态分配器

这玩意最直接的用法就是控制容器的内存,打个比方在我使用vector容器时一般情况下会这么用

#include<iostream>
#include <vector>
using namespace std;
class node{
public:
    node(){
        cout << "build node!" <<endl;
    }
};
int main(){
    vector<node> arr;
    arr.resize(10);
    return 0;
}

不难发现这个这个类构造了10个对象,但是众所周知vector 存储元素的实际位置是在堆上,并且vector在资源不足时会自动进行扩容以及值拷贝操作。频繁的申请与释放内存会导致内存碎片的产生。那我们有什么办法去控制vector构建对象时的行为呢?

控制数据位置

按照传统的方法,就是我们要自己实现一个内存分配器。作为vector的模版参数传入。(gpt真好用)

#include <iostream>
#include <vector>
#include <memory>   // 包含 std::allocator

// 自定义分配器类
template <typename T>
class MyAllocator {
public:
    // 类型定义
    using value_type = T;
    // 分配内存
    T* allocate(std::size_t n) {}
    // 释放内存
    void deallocate(T* p, std::size_t n) {}
    // 构造对象
    template <typename... Args>
    void construct(T* p, Args&&... args) {}
    // 销毁对象
    void destroy(T* p) {}
};
int main() {
    // 使用自定义分配器创建 vector
    std::vector<int, MyAllocator<int>> myVector;
    return 0;
}

这个时候果子哥就要说了,这玩意写起来太麻烦了,有没有什么简单的方法呢?
铁牛牛肉面 ,pmr就出现了

#include <iostream>
#include <string>
#include <vector>
#include <array>
#include <cstdlib>  // for byte
#include <memory_resource>
using namespace std;
int main()
{
    // 在栈上分配一些内存:
    array<byte, 200000> buf;
    // 将它用作vector的初始内存池:
    pmr::monotonic_buffer_resource pool{buf.data(), buf.size()};
    pmr::vector<string> coll{&pool};
    for (int i = 0; i < 1000; ++i) {
        coll.emplace_back("just a non-SSO string");
    }
    cout << buf.begin() << " " << buf.end() <<endl;
    cout << coll.begin().base() << " " << coll.end().base() <<endl;
    return 0;
}

程序的输出可以看到,coll的data位于buf中

0x7ffd6d151210 0x7ffd6d181f50
0x7ffd6d1591f0 0x7ffd6d160ef0

通过这种方式,我们就可以控制vector的数据存放地址了。

控制内存资源的一些行为

这个时候果子哥又要问了要是vector在重新分配内存时,超出了缓存区的大小,它会怎么处理呢?
我不吃牛肉 ,看代码

#include <iostream>
#include <string>
#include <vector>
#include <array>
#include <cstdlib>  // for byte
#include <memory_resource>
using namespace std;
int main()
{
    // 在栈上分配一些内存:
    array<byte, 20> buf;
    // 将它用作vector的初始内存池:
    pmr::monotonic_buffer_resource pool{buf.data(), buf.size()};
    pmr::vector<string> coll{&pool};
    for (int i = 0; i < 1000; ++i) {
        coll.emplace_back("just a non-SSO string");
    }
    cout << buf.begin() << " " << buf.end() <<endl;
    cout << coll.begin().base() << " " << coll.end().base() <<endl;
    return 0;
}

他还是会重新在堆上分配内存,以保证容器的正常功能。但是我们都已经使用内存池了,自然就可以在内存不够时进行一些操作。比如说在内存资源不够时抛出异常

#include <iostream>
#include <string>
#include <vector>
#include <array>
#include <cstdlib>  // for byte
#include <memory_resource>
using namespace std;
int main()
{
    // 在栈上分配一些内存:
    array<byte, 20> buf;
    // 将它用作vector的初始内存池:
    pmr::monotonic_buffer_resource pool{buf.data(), buf.size(),pmr::null_memory_resource()};
    pmr::vector<string> coll{&pool};
    for (int i = 0; i < 1000; ++i) {
        coll.emplace_back("just a non-SSO string");
    }
    cout << buf.begin() << " " << buf.end() <<endl;
    cout << coll.begin().base() << " " << coll.end().base() <<endl;
    return 0;
}

标准内存资源

刚刚在上面提到的 null_memory_resource()就是标准内存资源中的一种

标准内存资源 行为
new_delete_resource() 返回一个调用new和delete的内存资源的指针
synchronized_pool_resource 创建更少碎片化的、线程安全的内存资源的类
unsynchronized_pool_resource 创建更少碎片化的、线程不安全的内存资源的类
monotonic_buffer_resource 创建从不释放、可以传递一个可选的缓冲区、线程不安全的类
null_memory_resource() 返回一个每次分配都会失败的内存资源的指针

null_memory_resource

上方的代码,当buf中的内存空间调用不足后会向传入的标准内存资源对象申请资源。即向null_memory_resource申请新资源,而null_memory_resource申请失败后buf会抛出异常。

synchronized_pool_resource和unsynchronized_pool_resource

synchronized_pool_resource和unsynchronized_pool_resource是 尝试在相邻位置分配所有内存的内存资源类。因此,使用它们可以尽可能的减小内存碎片。

它们的不同在于synchronized_pool_resource是线程安全的(性能会差一些), 而unsynchronized_pool_resource不是。 因此,如果你知道这个池里的内存只会 被单个线程访问(或者分配和释放操作都是同步的),那么使用unsynchronized_pool_resource 将是更好的选择。

这两个类都使用底层的内存资源来进行实际的分配和释放操作。它们只是保证内存分配更加密集的包装。因此,

std::pmr::synchronized_pool_resource myPool;

等价于

std::pmr::synchronized_pool_resource myPool{std::pmr::get_default_resource()};

monotonic_buffer_resource

monotonic_buffer_resource这个就很有意思,它在每次申请资源时会直接在已分配的内存空间末尾追加,而不会处理之前已经分配了的空间

#include <memory_resource>
#include <vector>
#include <iostream>
using namespace std;
int main() {
    // 创建一个静态缓冲区
    int buf[1024] = {0};

    // 创建一个 monotonic_buffer_resource,并使用静态缓冲区
    pmr::monotonic_buffer_resource resource(buf, sizeof(buf));
    // 创建一个使用该内存资源的 vector
    pmr::vector<int> vec(&resource);
    vec.push_back(0);
    cout<< (unsigned long long)vec.data() - (unsigned long long )buf << endl;
    // 向 vector 添加数据
    for (int i = 1; i < 10; ++i) {
        vec.push_back(i);
    }
    unsigned long long off = (unsigned long long)vec.data() - (unsigned long long )buf;
    cout<< off << endl;
    for(int i=0;i< off/sizeof(int) + vec.size();i++){
        cout<< buf[i] << " ";
    }
    cout << endl;
    return 0;
}
0
60
0 0 1 0 1 2 3 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7 8 9

通过这段代码可以发现,vector在变更容量大小时,计划释放的内存没有做任何处理,而是直接在已分配的内存资源的最后追加了一段资源分给vector。

通过这种方式虽然会造成内存资源的浪费,但是以此换来的是简单的处理逻辑与极快的速度。

默认内存资源

内存资源 行为
get_default_resource() 返回一个指向当前默认内存资源的指针
set_default_resource(memresPtr) 设置默认内存资源(传递一个指针)并返回之前的内存资源的指针

我们可以用std::pmr::get_default_resource()获取当前的默认资源,然后传递它来初始化一个多态分配器。也可以使用std::set_default_resource() 来全局地设置一个不同的默认内存资源。这个资源将在任何作用域中用作默认资源,直到下一次调用

static std::pmr::synchronized_pool_resource myPool;
// 设置myPool为新的默认内存资源:
std::pmr::memory_resource* old = std::pmr::set_default_resource(&myPool);
...
// 恢复旧的默认内存资源:
std::pmr::set_default_resource(old);

默认资源

当set_default_resource没有被调用时会默认使用new_delete_resource()这个内存资源,他的行为跟正常的new,delete操作一样。

  • 每次向其申请内存时会调用new
  • 每次释放内存时会调用delete
    然而,注意持有这种内存资源的多态分配器不能和默认的分配器互换,因为它们的类型不同。因此:
    std::string s{"my string with some value"};
    std::pmr::string ps{std::move(s), std::pmr::new_delete_resource()}; // 拷贝
    将不会发生move(直接把s分配的内存转让给ps),而是把s的内存拷贝到ps内部用new分配的新的内存中。(只要是不同类型的多态分配器在std::move时都会进行值拷贝)

设置默认资源

如果你在程序中设置了自定义内存资源并且把它用作默认资源, 那么直接在main()中首先将它创建为static对象将是一个好方法:

int main()
{
    static std::pmr::synchronized_pool_resource myPool;
    ...
}

或者,提供一个返回静态资源的全局函数:

memory_resource* myResource()
{
    static std::pmr::synchronized_pool_resource myPool;
    return &myPool;
}

返回类型memory_resource是任何内存资源的基类。

注意之前的默认内存资源可能仍然会被使用,即使它已经被替换掉。 除非你知道(并且确信)不会发生这种情况,也就是没有使用该内存资源创建静态对象, 否则你应该确保你的资源的生命周期尽可能的长 (再提醒一次,可以在main()开始处创建它,这样它会在程序的最后才会被销毁)。

定义自定义内存资源

你现在可以提供自定义内存资源。要想这么做,你需要:

  • std::pmr::memory_resource派生
  • 实现下列私有函数
    • do_allocate()来分配内存
    • do_deallocate()来释放内存
    • do_is_equal()来定义什么情况下何时你的类型可以和其他内存资源对象交换分配的内存

(居然是继承的方式,我还以为会用SFINAE那一套去搞呢,concept 是C++20出的啊,那没事了)

这里有一个让我们可以追踪所有内存资源的分配和释放操作的完整示例:

#include <iostream>
#include <string>
#include <memory_resource>

class Tracker : public std::pmr::memory_resource
{
private:
    std::pmr::memory_resource *upstream;    // 被包装的内存资源
    std::string prefix{};
public:
    // 包装传入的或者默认的资源:
    explicit Tracker(std::pmr::memory_resource *us
            = std::pmr::get_default_resource()) : upstream{us} {
    }

    explicit Tracker(std::string p, std::pmr::memory_resource *us
            = std::pmr::get_default_resource()) : upstream{us}, prefix{std::move(p)} {
    }
private:
    void* do_allocate(size_t bytes, size_t alignment) override {
        std::cout << prefix << "allocate " << bytes << " Bytes\n";
        void* ret = upstream->allocate(bytes, alignment);
        return ret;
    }
    void do_deallocate(void* ptr, size_t bytes, size_t alignment) override {
        std::cout << prefix << "deallocate " << bytes << " Bytes\n";
        upstream->deallocate(ptr, bytes, alignment);
    }
    bool do_is_equal(const std::pmr::memory_resource& other) const noexcept override {
        // 是同一个对象?
        if (this == &other)
            return true;
        // 是相同的类型并且prefix和upstream都相等?
        auto op = dynamic_cast<const Tracker*>(&other);
        return op != nullptr && op->prefix == prefix && upstream->is_equal(other);
    }
};

就像通常的智能内存资源一样,我们支持传递另一个内存资源(通常叫做upstream) 来包装它或者将它用作备选项。另外,我们可以传递一个可选的前缀。 每一次输出分配或释放操作信息时都会先输出这个可选的前缀。

我们唯一需要实现的其他函数是do_is_equal(),它定义了何时两个内存资源可以交换 (即一个多态内存资源对象是否以及何时可以释放另一个多态内存资源对象分配的内存)。 在这里,我们简单的认为,只要前缀相同,这种类型的对象都可以释放另一个该类型对象分配的内存:

支持多态分配器

想要支持多态分配器出乎意料的简单, 你只需要:

  • 定义一个public成员allocator_type作为一个多态分配器
  • 为所有构造函数添加一个接受分配器作为额外参数的重载(包括拷贝和移动构造函数)
  • 给初始化用的构造函数的分配器参数添加一个默认的allocator_type类型的值 ( 不 包括拷贝和移动构造函数)
#include <string>
#include <memeory_resource>

// 支持多态分配器的顾客类型
// 分配器存储在字符串成员中
class PmrCustomer
{
private:
    std::pmr::string name;  // 也可以用来存储分配器
public:
    using allocator_type = std::pmr::polymorphic_allocator<char>;

    // 初始化构造函数:
    PmrCustomer(std::pmr::string n, allocator_type alloc = {}) : name{std::move(n), alloc} {
    }

    // 带有分配器的移动,构造函数:
    PmrCustomer(const PmrCustomer& c, allocator_type alloc) : name{c.name, alloc} {
    }

    PmrCustomer(PmrCustomer&& c, allocator_type alloc) : name{std::move(c.name), alloc} {
    }

    // setter/getter:
    void setName(std::pmr::string s) {
        name = std::move(s);
    }
    std::pmr::string getName() const {
        return name;
    }
    std::string getNameAsString() const {
        return std::string{name};
    }
};

文章作者: xucanxx
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 xucanxx !
  目录