0%

深入理解std::allocator

请求堆上分配内存需要操作系统的参与,频繁的向请求内存开销很大。为了优化这个问题,可以把预先分配一部分内存放在池子里。

C++在堆上创建对象一般是用newnew的执行包含了两个过程:

  1. 分配内存
  2. 初始化对象

delete的执行也包含了两个过程:

  1. 对象析构
  2. 释放内存

当创建单个对象的时候,我们拥有构造对象所需的值,我们通常希望内存分配和对象初始化组合在一起;但是连续分配一堆对象的时候,我们往往希望分配内存和对象初始化分离,先分配一大快内存,在需要时再按需执行对象的初始化,此时如果将内存分配和对象初始化放在一起则会导致不必要的性能开销。例如new std::vector<T>(10),我们希望只分配10个T类型的空间,而不执行T的默认构造方法,只有在emplace_back的时候才对内存初始化。

标准库中提供的std::allocator<T>模板类允许我们将内存分配和对象初始化分离,使用std::allocator<T>通常会提供更好的性能和更灵活的内存管理能力。

std::allocator<T>定义在头文件<memory>中,提供一种类型感知的内存分配方法,它分配的内存是原始的、未构造的。

allocator分配的内存是未构造的(unconstructed)。我们按需要在此内存中构造对象。在新标准库中,construct成员函数接受一个指针和零个或多个额外参数,在给定位置构造一个元素。额外参数用来初始化构造的对象。类似make_shared的参数,这些额外参数必须是与构造的对象的类型相匹配的合法的初始化器。

在早期版本的标准库中,construct只接受两个参数:指向创建对象位置的指针和一个元素类型的值。因此,我们只能将一个元素拷贝到未构造空间中,而不能用元素类型的任何其它构造函数来构造一个元素。还未构造对象的情况下就使用原始内存是错误的。为了使用allocator返回的内存,我们必须用construct构造对象。使用未构造的内存,其行为是未定义的。

std::allocator<T>支持的方法如下:

方法 说明
std::allocator<T>() 构造一个能够分配T类型对象的allocator对象
T* allocate(int n) 分配一段原始的未初始化的内存,长度为 n * sizeof(T),返回分配的地址
deallocate(T* p, n) 释放指针p开始的,长度为n * sizeof(T)的连续内存空间。p必须是allocate返回的地址,且n必须是p指向内存创建时候的大小。调用之前必须先调用destroy()
construct(T* p, args...) p指向的地址处用args参数列表构造T类型对象
destroy(T* p) p指向的地址的对象执行析构
flowchart LR
  subgraph new [new]
    allocate --> construct
  end
  subgraph delete [delete]
    destroy --> deallocate
  end
  construct --> destroy

使用std::allocator<T>来管理一个对象的生命周期时,先用allocate分配一段原始的、未初始化的内存,然后用construct()在这段内存上初始化对象。用destroy()析构对象,析构后的内存可以用deallocate()归还给操作系统,也可以再次用construct()在上面构造其他对象。

传递给deallocate()的指针不能为空,它必须指向由allocate()分配的内存。而且,传递给deallocate()的大小参数必须与调用allocate()分配内存时提供的大小参数具有一样的值

标准库还为std::allocator<T>类定义了两个伴随算法,可以在迭代器指向的未初始化内存中创建对象:

函数 说明
uninitialized_copy(std::iterator<T> begin, std::iterator<T> end, std::iterator<T> iter) 从迭代器beginend之间的内存依次拷贝到iter开始的的未初始化的内存中
uninitialized_copy_n(std::iterator<T> begin, int n, std::iterator<T> iter) 从迭代器begin开始,拷贝n个元素到iter开始的内存中
uninitialized_fill(std::iterator<T> begin, std::iterator<T> end, T v) beginend之间的内容用v填充
uninitialized_fill_n(std::iterator<T> begin, int n, T v) begin开始的位置依次拷贝n个值为v的对象

std::allocator<T>是STL容器的默认内存分配器。你还可以替换自己的分配器,这允许你控制标准容器分配内存的方式。

例子:

/*================================================================
* Copyright (C) 2022 XUranus All rights reserved.
*
* File: test_allocator.cpp
* Author: XUranus
* Date: 2022-06-17-
* Description:
*
================================================================*/

#include <memory>
#include <iostream>
#include <string>
#include <vector>
#include <cassert>

void TEST1() {
std::allocator<std::string> strAlloc;
std::string *str = strAlloc.allocate(3);
std::string *p = str;

strAlloc.construct(p); // ""
assert(p->empty());

strAlloc.construct(++p, 5, 'a'); // "aaaaa"
assert(*p == "aaaaa");

strAlloc.construct(++p, "Hello"); // "Hello"
assert(*p == "Hello");

strAlloc.destroy(str + 1);
strAlloc.construct(str + 1, "new string");
assert(*(str + 1) == "new string");


strAlloc.destroy(str);
strAlloc.destroy(str + 1);
strAlloc.destroy(str + 2);
strAlloc.deallocate(str, 3);
}

void TEST2() {
std::vector<int> vec {1,2,3};
std::allocator<int> intAlloc;
int* p = intAlloc.allocate(6); // p = { ?, ?, ?, ?, ?, ? }

int* q = std::uninitialized_copy(vec.begin(), vec.end(), p); // p = { 1, 2, 3, ?, ?, ? }
std::uninitialized_fill_n(q, 3, 10); // p = { 1, 2, 3, 10, 10, 10};
assert(*p == 1);
assert(*(p + 3) == 10);

intAlloc.deallocate(p, 6);
}

int main() {
TEST1();
TEST2();
std::cout << "SUCESS" << std::endl;
return 0;
}

参考资料

Disqus评论区没有正常加载,请使用科学上网