这给了我一个更多地使用分配器的借口。我选择了多态分配器——尽管这只是切线相关¹。
旁白:这种关系是,使用自定义分配器时,您经常希望将分配器传播到可识别分配器的嵌套类型。请参阅下面的“高级”
示例元素类型
struct X {
std::string key, value;
};
它并没有变得更简单,尽管它允许我们尝试分享
稍后带有嵌套字符串的分配器。
跟踪内存资源
让我们创建一个跟踪内存资源。这很简单,我们只转发标准的new/delete:
namespace pmr = boost::container::pmr;
struct tracing_resource : pmr::memory_resource {
uint64_t n = 0, total_bytes = 0;
virtual void* do_allocate(std::size_t bytes, std::size_t alignment) override {
n += 1;
total_bytes += bytes;
return pmr::new_delete_resource()->allocate(bytes, alignment);
}
virtual void do_deallocate(void* p, std::size_t bytes, std::size_t alignment) override {
if (p) {
n -= 1;
total_bytes -= bytes;
}
return pmr::new_delete_resource()->deallocate(p, bytes, alignment);
}
virtual bool do_is_equal(const memory_resource& other) const noexcept override {
return pmr::new_delete_resource()->is_equal(other);
}
};
我们可以检查n(分配数量)以及在整个测试代码中不同点分配的总字节数。
测试程序
让我们把它放在main 中,从我们的跟踪器开始:
tracing_resource tracer;
让我们在上面挂载一个池化资源:
pmr::unsynchronized_pool_resource res(&tracer);
auto allocations = [&] {
fmt::print("alloc: #{}, {} bytes, cached = {}\n", tracer.n, tracer.total_bytes, cache_buckets(res));
};
allocations();
这将打印出来
alloc: #0, 0 bytes, cached = {0, 0, 0, 0, 0, 0, 0, 0, 0}
就在门口。
现在,让我们开始(重新)以各种模式分配一些双端队列:
pmr::deque<X> collection(&res);
auto report = [&] {
fmt::print("collection = {}\nalloc: #{}, {} bytes, cached = {}\n", collection, tracer.n, tracer.total_bytes, cache_buckets(res));
};
std::vector data1 { X{"1", "eins"}, {"2", "zwei"}, {"3", "drei"}, };
std::vector data2 { X{"4", "vier"}, {"5", "fuenf"}, {"6", "sechs"}, };
std::vector data3 { X{"7", "sieben"}, {"8", "acht"}, {"9", "neun"}, };
auto i = 0;
for (auto const& data : {data1, data2, data3}) {
for (auto el : data) {
(i%2)
? collection.push_back(el)
: collection.push_front(el);
}
report();
collection.clear();
report();
}
这将在容器的不同末端附加不同的序列。我们不会
做很多突变,因为当我们制作字符串时这会变得有趣
使用池化资源)。
现场演示
Live On Compiler Explorer
#include <boost/container/pmr/deque.hpp>
#include <boost/container/pmr/unsynchronized_pool_resource.hpp>
// debug output
#include <range/v3/all.hpp>
#include <fmt/ranges.h>
#include <fmt/ostream.h>
#include <iomanip>
namespace pmr = boost::container::pmr;
struct tracing_resource : pmr::memory_resource {
uint64_t n = 0, total_bytes = 0;
virtual void* do_allocate(std::size_t bytes, std::size_t alignment) override {
n += 1;
total_bytes += bytes;
return pmr::new_delete_resource()->allocate(bytes, alignment);
}
virtual void do_deallocate(void* p, std::size_t bytes, std::size_t alignment) override {
if (p) {
n -= 1;
total_bytes -= bytes;
}
return pmr::new_delete_resource()->deallocate(p, bytes, alignment);
}
virtual bool do_is_equal(const memory_resource& other) const noexcept override {
return pmr::new_delete_resource()->is_equal(other);
}
};
struct X {
std::string key, value;
friend std::ostream& operator<<(std::ostream& os, X const& x) {
return os << "(" << std::quoted(x.key) << ", " << std::quoted(x.value) << ")";
}
};
auto cache_buckets(pmr::unsynchronized_pool_resource& res) {
using namespace ::ranges;
return views::iota(0ull)
| views::take_exactly(res.pool_count())
| views::transform([&](auto idx) {
return res.pool_cached_blocks(idx);
});
}
int main() {
tracing_resource tracer;
{
pmr::unsynchronized_pool_resource res(&tracer);
auto allocations = [&] {
fmt::print("alloc: #{}, {} bytes, cached = {}\n", tracer.n, tracer.total_bytes, cache_buckets(res));
};
allocations();
{
pmr::deque<X> collection(&res);
auto report = [&] {
fmt::print("collection = {}\nalloc: #{}, {} bytes, cached = {}\n", collection, tracer.n, tracer.total_bytes, cache_buckets(res));
};
std::vector data1 { X{"1", "eins"}, {"2", "zwei"}, {"3", "drei"}, };
std::vector data2 { X{"4", "vier"}, {"5", "fuenf"}, {"6", "sechs"}, };
std::vector data3 { X{"7", "sieben"}, {"8", "acht"}, {"9", "neun"}, };
auto i = 0;
for (auto const& data : {data1, data2, data3}) {
for (auto el : data) {
(i%2)
? collection.push_back(el)
: collection.push_front(el);
}
report();
collection.clear();
report();
}
}
allocations();
}
fmt::print("alloc: #{}, {} bytes\n", tracer.n, tracer.total_bytes);
}
打印
alloc: #0, 0 bytes, cached = {0, 0, 0, 0, 0, 0, 0, 0, 0}
collection = {("3", "drei"), ("2", "zwei"), ("1", "eins")}
alloc: #4, 1864 bytes, cached = {0, 0, 0, 0, 0, 1, 0, 0, 0}
collection = {}
alloc: #4, 1864 bytes, cached = {0, 0, 0, 0, 0, 2, 0, 0, 0}
collection = {("6", "sechs"), ("5", "fuenf"), ("4", "vier")}
alloc: #4, 1864 bytes, cached = {0, 0, 0, 0, 0, 2, 0, 0, 0}
collection = {}
alloc: #4, 1864 bytes, cached = {0, 0, 0, 0, 0, 2, 0, 0, 0}
collection = {("9", "neun"), ("8", "acht"), ("7", "sieben")}
alloc: #4, 1864 bytes, cached = {0, 0, 0, 0, 0, 2, 0, 0, 0}
collection = {}
alloc: #4, 1864 bytes, cached = {0, 0, 0, 0, 0, 2, 0, 0, 0}
alloc: #4, 1864 bytes, cached = {0, 0, 1, 0, 0, 3, 0, 0, 0}
alloc: #0, 0 bytes
高级
正如所承诺的,我们可以让元素类型分配器感知并显示传播:
#include <boost/container/pmr/string.hpp>
// ...
struct X {
using allocator_type = pmr::polymorphic_allocator<X>;
template<typename K, typename V>
explicit X(K&& key, V&& value, allocator_type a = {})
: key(std::forward<K>(key), a), value(std::forward<V>(value), a) {}
pmr::string key, value;
};
让我们修改测试驱动程序以 emplace 字符串文字中的元素:
std::vector data1 { std::pair{"1", "eins"}, {"2", "zwei"}, {"3", "drei"}, };
std::vector data2 { std::pair{"4", "vier"}, {"5", "fuenf"}, {"6", "sechs"}, };
std::vector data3 { std::pair{"7", "sieben"}, {"8", "acht"}, {"9", "neun"}, };
auto i = 0;
for (auto const& data : {data1, data2, data3}) {
for (auto [k,v] : data) {
(i%2)
? collection.emplace_back(k, v)
: collection.emplace_front(k, v);
}
为了更好的衡量,让我们也改变嵌套字符串值之一:
collection.at(1).value.append(50, '*'); // thwart SSO
report();
collection.at(1).value = "sept";
report();
再次演示 Live On Compiler Explorer
#include <boost/container/pmr/deque.hpp>
#include <boost/container/pmr/string.hpp>
#include <boost/container/pmr/unsynchronized_pool_resource.hpp>
// debug output
#include <range/v3/all.hpp>
#include <fmt/ranges.h>
#include <fmt/ostream.h>
#include <iomanip>
namespace pmr = boost::container::pmr;
struct tracing_resource : pmr::memory_resource {
uint64_t n = 0, total_bytes = 0;
virtual void* do_allocate(std::size_t bytes, std::size_t alignment) override {
n += 1;
total_bytes += bytes;
return pmr::new_delete_resource()->allocate(bytes, alignment);
}
virtual void do_deallocate(void* p, std::size_t bytes, std::size_t alignment) override {
if (p) {
n -= 1;
total_bytes -= bytes;
}
return pmr::new_delete_resource()->deallocate(p, bytes, alignment);
}
virtual bool do_is_equal(const memory_resource& other) const noexcept override {
return pmr::new_delete_resource()->is_equal(other);
}
};
struct X {
using allocator_type = pmr::polymorphic_allocator<X>;
template<typename K, typename V>
explicit X(K&& key, V&& value, allocator_type a = {})
: key(std::forward<K>(key), a), value(std::forward<V>(value), a) {}
pmr::string key, value;
friend std::ostream& operator<<(std::ostream& os, X const& x) {
return os << "(" << std::quoted(x.key.c_str()) << ", " << std::quoted(x.value.c_str()) << ")";
}
};
auto cache_buckets(pmr::unsynchronized_pool_resource& res) {
using namespace ::ranges;
return views::iota(0ull)
| views::take_exactly(res.pool_count())
| views::transform([&](auto idx) {
return res.pool_cached_blocks(idx);
});
}
int main() {
tracing_resource tracer;
{
pmr::unsynchronized_pool_resource res(&tracer);
auto allocations = [&] {
fmt::print("alloc: #{}, {} bytes, cached = {}\n", tracer.n, tracer.total_bytes, cache_buckets(res));
};
allocations();
{
pmr::deque<X> collection(&res);
auto report = [&] {
fmt::print("collection = {}\nalloc: #{}, {} bytes, cached = {}\n", collection, tracer.n, tracer.total_bytes, cache_buckets(res));
};
std::vector data1 { std::pair{"1", "eins"}, {"2", "zwei"}, {"3", "drei"}, };
std::vector data2 { std::pair{"4", "vier"}, {"5", "fuenf"}, {"6", "sechs"}, };
std::vector data3 { std::pair{"7", "sieben"}, {"8", "acht"}, {"9", "neun"}, };
auto i = 0;
for (auto const& data : {data1, data2, data3}) {
for (auto [k,v] : data) {
(i%2)
? collection.emplace_back(k, v)
: collection.emplace_front(k, v);
}
report();
collection.at(1).value.append(50, '*'); // thwart SSO
report();
collection.at(1).value = "sept";
report();
collection.clear();
report();
}
}
allocations();
}
fmt::print("alloc: #{}, {} bytes\n", tracer.n, tracer.total_bytes);
}
打印:
alloc: #0, 0 bytes, cached = {0, 0, 0, 0, 0, 0, 0, 0, 0}
collection = {("3", "drei"), ("2", "zwei"), ("1", "eins")}
alloc: #4, 1864 bytes, cached = {0, 0, 0, 0, 0, 1, 0, 0, 0}
collection = {("3", "drei"), ("2", "zwei**************************************************"), ("1", "eins")}
alloc: #5, 2008 bytes, cached = {0, 0, 0, 0, 0, 1, 0, 0, 0}
collection = {("3", "drei"), ("2", "sept"), ("1", "eins")}
alloc: #5, 2008 bytes, cached = {0, 0, 0, 0, 0, 1, 0, 0, 0}
collection = {}
alloc: #5, 2008 bytes, cached = {0, 0, 0, 1, 0, 2, 0, 0, 0}
collection = {("6", "sechs"), ("5", "fuenf"), ("4", "vier")}
alloc: #5, 2008 bytes, cached = {0, 0, 0, 1, 0, 2, 0, 0, 0}
collection = {("6", "sechs"), ("5", "fuenf**************************************************"), ("4", "vier")}
alloc: #5, 2008 bytes, cached = {0, 0, 0, 0, 0, 2, 0, 0, 0}
collection = {("6", "sechs"), ("5", "sept"), ("4", "vier")}
alloc: #5, 2008 bytes, cached = {0, 0, 0, 0, 0, 2, 0, 0, 0}
collection = {}
alloc: #5, 2008 bytes, cached = {0, 0, 0, 1, 0, 2, 0, 0, 0}
collection = {("9", "neun"), ("8", "acht"), ("7", "sieben")}
alloc: #5, 2008 bytes, cached = {0, 0, 0, 1, 0, 1, 0, 0, 0}
collection = {("9", "neun"), ("8", "acht**************************************************"), ("7", "sieben")}
alloc: #5, 2008 bytes, cached = {0, 0, 0, 0, 0, 1, 0, 0, 0}
collection = {("9", "neun"), ("8", "sept"), ("7", "sieben")}
alloc: #5, 2008 bytes, cached = {0, 0, 0, 0, 0, 1, 0, 0, 0}
collection = {}
alloc: #5, 2008 bytes, cached = {0, 0, 0, 1, 0, 2, 0, 0, 0}
alloc: #5, 2008 bytes, cached = {0, 0, 1, 1, 0, 3, 0, 0, 0}
alloc: #0, 0 bytes
结论
虽然我选择了多态分配器,因为它们默认支持scoped_allocator_adaptor<> 样式的传播,但以上所有内容都可以使用静态类型的分配器创建。
它确实表明,如果您使用池分配器,则双端队列行为将变为池化。
旁白:存在能够放弃清理的池分配器,这在某些情况下可能是有效的,例如无论如何,整个内存池都位于堆栈上。这是一种常见的分配优化技术,允许跳过大量的释放/销毁。
解决您列表中的更多问题:
-
Q. 如何为 boost::container::deque 指定块分配器
(块,而不是物品!)?
A. 我认为分配器本质上总是被调用块,
因为双端队列的工作方式。
-
Q.如果有办法,那么这样的规范会支持分配器吗
有状态吗?
A. 标准库和 Boost Container 都应该支持
这些天有状态的分配器。如有疑问,Boost Container 有您的
返回。
-
问。如果是,那么上面提到的分配器会去吗?
A.我没仔细看,但你可以把它放在同一个测试台上
找出来
-
Q. 毕竟,如果不是这样,我怎么能做一个不会
至少释放一个释放的块,并在稍后重用它
需要新的区块吗?
答。见上文。我不确定我是否理解“在
至少一个”子句,但我确实注意到 Boost 的双端队列实现确实可以
一个“私人地图”分配——大概是为了一些块记账开销
- 一直存在直到 deque 对象被销毁。本次分配
不会在(默认)构造时发生,而是在以后发生。