如何在将 std::vector 与自定义池分配器一起使用时摆脱无用的分配和构造?
How to get rid of useless allocations and constructions while using std::vector with custom pool allocator?
我有一个自定义池分配器,我希望它与 std::vector
一起使用
#include <iostream>
#include <memory>
#include <vector>
#include <type_traits>
template<typename T, uint Size>
struct ObjectPool
{
using value_type = T;
using pointer = value_type *;
ObjectPool()
{
for (auto i = 1; i < Size; ++i)
buffer[i - 1].next = &buffer[i];
nextFreeItem = &buffer[0];
}
ObjectPool(const ObjectPool&) = delete;
ObjectPool(ObjectPool&& other) noexcept
: buffer{ std::move(other.buffer) }
, nextFreeItem{ other.nextFreeItem }
{
other.nextFreeItem = nullptr;
}
~ObjectPool() = default;
template<typename U>
struct rebind
{
typedef ObjectPool<U, Size> other;
};
template<typename U, uint other_capacity>
ObjectPool(const ObjectPool<U, other_capacity>& other) {}
[[nodiscard]] pointer allocate(uint size = 0)
{
std::cout << "ObjectPool: allocate " << size << "\n";
if (nextFreeItem == nullptr)
throw std::bad_alloc{};
const auto item = nextFreeItem;
nextFreeItem = item->next;
return reinterpret_cast<pointer>(&item->storage);
}
void deallocate(pointer p, uint = 0) noexcept
{
std::cout << "ObjectPool: deallocate\n";
const auto item = reinterpret_cast<Item*>(p);
item->next = nextFreeItem;
nextFreeItem = item;
}
template<typename U, typename ...Args>
void construct(U* mem, Args&& ...args)
{
std::cout << "ObjectPool: construct\n";
new (mem) value_type(std::forward<Args>(args)...);
}
template<typename U>
void destroy(U* mem) noexcept
{
std::cout << "ObjectPool: destroy\n";
if (mem == nullptr)
return;
mem->~value_type();
}
ObjectPool& operator =(const ObjectPool&) = delete;
ObjectPool& operator =(ObjectPool&& other) noexcept
{
if (this == &other)
return *this;
buffer = std::move(other.buffer);
nextFreeItem = other.nextFreeItem;
other.nextFreeItem = nullptr;
return *this;
}
private:
union Item
{
std::aligned_storage_t<sizeof(value_type), alignof(value_type)> storage;
Item* next;
};
std::unique_ptr<Item[]> buffer = std::make_unique<Item[]>(Size);
Item* nextFreeItem = nullptr;
};
int main()
{
std::vector<int, ObjectPool<int, 5>> pool;
pool.push_back(5);
pool.push_back(3);
pool.push_back(523);
for(const auto& p : pool) {
std::cout << p << std::endl;
}
pool.pop_back();
for(const auto& p : pool) {
std::cout << p << std::endl;
}
}
这个程序的输出是
- ObjectPool: 分配 1
- 对象池:构造
- 对象池:分配 2
- 对象池:构造
- 对象池:构造
- 对象池:销毁
- 对象池:解除分配
- 对象池:分配 3
- 对象池:构造
- 对象池:构造
- 对象池:构造
- 对象池:销毁
- 对象池:销毁
- 对象池:解除分配
- 523
- 3
- -539300144
- 对象池:销毁
- 523
- 3
- 对象池:销毁
- 对象池:销毁
- 对象池:解除分配
我希望它是
ObjectPool: allocate whatever // this is space for 5
ObjectPool: construct // constructs 5
ObjectPool: allocate whatever // this is space for 3
ObjectPool: construct // constructs 3
ObjectPool: allocate whatever // this is space for 523
ObjectPool: construct // constructs 523, but actual output gives garbage value
ObjectPool: destroy // destroys 523
ObjectPool: deallocate // deallocates 523
ObjectPool: destroy // destroys 3
ObjectPool: destroy // destroys 5
ObjectPool: deallocate // deallocates 3 and 5
正如你所看到的,构造方法甚至被调用了 3 次,而它只需要调用一次。
为什么523是垃圾?
如何在不执行 pool.reserve(5)
的情况下实现预期的输出?
可能吗?
传递给ObjectPool::allocate
的值是将连续存储在内存中的对象数。这意味着当 allocator(2)
被调用时,你需要 return 一个指向至少有 2 * sizeof(T)
个块的块的指针。您的分配器只有 return 一个指向单个块的指针。当向量构造函数将第二个(或第三个)元素添加到新生成的向量中时,它将覆盖未专门分配的内存。下一次调用 allocator
将分配该内存,导致向量损坏。
矢量的分配内存是连续的。当您第一次调用 push_back
时,会为向量分配一个元素(其容量为 1)。这将生成输出的第 1-2 行。
第二次调用push_back
时,由于向量的容量已满,将请求一个新块。这会生成输出的第 2-7 行。第 4 行正在将现有元素复制到新内存块,第 5 行正在构造刚刚添加的新元素,第 6 行正在从原始内存块中销毁该元素。第 7 行是释放原始内存块的时间(returned 到分配器)。向量的容量将为 2.
下一次调用 push_back
将再次导致矢量大小调整,生成输出的第 8-14 行。第 9-10 行将现有元素复制到新内存块,第 11 行构造新添加的元素,第 12-13 行在旧内存块中销毁它们,第 14 return 行将旧内存块发送给分配器。
以下行的输出已损坏,因为稍后对分配器的调用 return 指向向量对象正在使用的内存。由此产生的数据复制移动了不正确或损坏的数据。
解决方案是让您的 allocate
函数保留适当数量的块。 (所以 allocate(2)
应该前进 nextFreeItem
两个区块,假设它前进的两个区块是连续的。)
我有一个自定义池分配器,我希望它与 std::vector
一起使用#include <iostream>
#include <memory>
#include <vector>
#include <type_traits>
template<typename T, uint Size>
struct ObjectPool
{
using value_type = T;
using pointer = value_type *;
ObjectPool()
{
for (auto i = 1; i < Size; ++i)
buffer[i - 1].next = &buffer[i];
nextFreeItem = &buffer[0];
}
ObjectPool(const ObjectPool&) = delete;
ObjectPool(ObjectPool&& other) noexcept
: buffer{ std::move(other.buffer) }
, nextFreeItem{ other.nextFreeItem }
{
other.nextFreeItem = nullptr;
}
~ObjectPool() = default;
template<typename U>
struct rebind
{
typedef ObjectPool<U, Size> other;
};
template<typename U, uint other_capacity>
ObjectPool(const ObjectPool<U, other_capacity>& other) {}
[[nodiscard]] pointer allocate(uint size = 0)
{
std::cout << "ObjectPool: allocate " << size << "\n";
if (nextFreeItem == nullptr)
throw std::bad_alloc{};
const auto item = nextFreeItem;
nextFreeItem = item->next;
return reinterpret_cast<pointer>(&item->storage);
}
void deallocate(pointer p, uint = 0) noexcept
{
std::cout << "ObjectPool: deallocate\n";
const auto item = reinterpret_cast<Item*>(p);
item->next = nextFreeItem;
nextFreeItem = item;
}
template<typename U, typename ...Args>
void construct(U* mem, Args&& ...args)
{
std::cout << "ObjectPool: construct\n";
new (mem) value_type(std::forward<Args>(args)...);
}
template<typename U>
void destroy(U* mem) noexcept
{
std::cout << "ObjectPool: destroy\n";
if (mem == nullptr)
return;
mem->~value_type();
}
ObjectPool& operator =(const ObjectPool&) = delete;
ObjectPool& operator =(ObjectPool&& other) noexcept
{
if (this == &other)
return *this;
buffer = std::move(other.buffer);
nextFreeItem = other.nextFreeItem;
other.nextFreeItem = nullptr;
return *this;
}
private:
union Item
{
std::aligned_storage_t<sizeof(value_type), alignof(value_type)> storage;
Item* next;
};
std::unique_ptr<Item[]> buffer = std::make_unique<Item[]>(Size);
Item* nextFreeItem = nullptr;
};
int main()
{
std::vector<int, ObjectPool<int, 5>> pool;
pool.push_back(5);
pool.push_back(3);
pool.push_back(523);
for(const auto& p : pool) {
std::cout << p << std::endl;
}
pool.pop_back();
for(const auto& p : pool) {
std::cout << p << std::endl;
}
}
这个程序的输出是
- ObjectPool: 分配 1
- 对象池:构造
- 对象池:分配 2
- 对象池:构造
- 对象池:构造
- 对象池:销毁
- 对象池:解除分配
- 对象池:分配 3
- 对象池:构造
- 对象池:构造
- 对象池:构造
- 对象池:销毁
- 对象池:销毁
- 对象池:解除分配
- 523
- 3
- -539300144
- 对象池:销毁
- 523
- 3
- 对象池:销毁
- 对象池:销毁
- 对象池:解除分配
我希望它是
ObjectPool: allocate whatever // this is space for 5
ObjectPool: construct // constructs 5
ObjectPool: allocate whatever // this is space for 3
ObjectPool: construct // constructs 3
ObjectPool: allocate whatever // this is space for 523
ObjectPool: construct // constructs 523, but actual output gives garbage value
ObjectPool: destroy // destroys 523
ObjectPool: deallocate // deallocates 523
ObjectPool: destroy // destroys 3
ObjectPool: destroy // destroys 5
ObjectPool: deallocate // deallocates 3 and 5
正如你所看到的,构造方法甚至被调用了 3 次,而它只需要调用一次。
为什么523是垃圾?
如何在不执行 pool.reserve(5)
的情况下实现预期的输出?
可能吗?
传递给ObjectPool::allocate
的值是将连续存储在内存中的对象数。这意味着当 allocator(2)
被调用时,你需要 return 一个指向至少有 2 * sizeof(T)
个块的块的指针。您的分配器只有 return 一个指向单个块的指针。当向量构造函数将第二个(或第三个)元素添加到新生成的向量中时,它将覆盖未专门分配的内存。下一次调用 allocator
将分配该内存,导致向量损坏。
矢量的分配内存是连续的。当您第一次调用 push_back
时,会为向量分配一个元素(其容量为 1)。这将生成输出的第 1-2 行。
第二次调用push_back
时,由于向量的容量已满,将请求一个新块。这会生成输出的第 2-7 行。第 4 行正在将现有元素复制到新内存块,第 5 行正在构造刚刚添加的新元素,第 6 行正在从原始内存块中销毁该元素。第 7 行是释放原始内存块的时间(returned 到分配器)。向量的容量将为 2.
下一次调用 push_back
将再次导致矢量大小调整,生成输出的第 8-14 行。第 9-10 行将现有元素复制到新内存块,第 11 行构造新添加的元素,第 12-13 行在旧内存块中销毁它们,第 14 return 行将旧内存块发送给分配器。
以下行的输出已损坏,因为稍后对分配器的调用 return 指向向量对象正在使用的内存。由此产生的数据复制移动了不正确或损坏的数据。
解决方案是让您的 allocate
函数保留适当数量的块。 (所以 allocate(2)
应该前进 nextFreeItem
两个区块,假设它前进的两个区块是连续的。)