有效地使用多个分配器

Using multiple allocators efficiently

本文关键字:分配器 有效地      更新时间:2023-10-16

我一直在研究将我的分配方法从简化重载new转换为通过代码库使用多个分配器。但是,如何有效地使用多个分配器?通过我的研究,我能设计的唯一方法就是让分配器成为全局分配器。尽管如此,这似乎有问题,因为使用许多全局变量通常是一个"坏主意"。

我想知道如何有效地使用多个分配器。例如,我可能有一个分配器只用于特定的子系统,而另一个分配器用于不同的子系统。我不确定是否唯一的方法是使用多个全局分配器,所以我希望有更好的见解和设计。

在C++2003中,分配器模型被破坏,并且没有真正合适的解决方案。对于C++2011,分配器模型是固定的,您可以拥有每个实例的分配器,这些分配器向下传播到包含的对象(当然,除非您选择替换它们)。通常,为了使其有用,您可能希望使用动态多态分配器类型,而默认的std::allocator<T>不需要是这种类型(通常我希望它不会是动态多态的,尽管这可能是更好的实现选择)。然而,标准C++库中几乎所有进行内存分配的类都是以分配器类型为模板参数的模板(例如,IOStreams是一个例外,但通常它们不会分配任何有趣的内存量来保证添加分配器支持)。

在您的一些评论中,您坚持分配器实际上需要是全局的:这绝对是不正确的。每个感知分配器的类型都存储给定分配器的一个副本(至少,如果它有任何实例级数据;如果没有,则没有任何内容可存储,例如,默认分配器使用operator new()operator delete()的情况)。这实际上意味着,只要有任何活动分配器在使用,给对象的分配机制就需要保持不变。这可以使用全局对象来完成,但也可以使用引用计数或将分配器与包含所有对象的对象相关联来完成。例如,如果每个"文档"(想想XML、Excel、Pages,无论什么结构的文件)都将分配器传递给其成员,则分配器可以作为文档的成员存在,并且在文档的所有内容都被销毁后,当文档被销毁时,分配器就会被销毁。分配器模型的这一部分应该与C++2011之前的类一起使用,只要它们也接受分配器参数即可。然而,在C++2011之前的类中,分配器不会传递给包含的对象。例如,如果为std::vector<std::string>提供分配器,则C++2011版本将使用为std::vector<std::string>提供的分配器创建std::strings,该分配器经过适当转换以处理std::strings。这在C++2011之前的分配器中不会发生。

要在子系统中实际使用分配器,您需要有效地传递它们,要么显式地将其作为函数和/或类的参数,要么通过充当上下文的分配器感知对象隐式地传递。例如,如果使用任何标准容器作为传递的上下文的[一部分],则可以使用其get_allocator()方法获得所使用的分配器。

您可以使用new放置。这既可以用于指定内存区域,也可以用于重载类型的static void* operator new(ARGS)。如果效率很重要,而你的问题又很棘手,那么全球化是不必要的,这真的是个坏主意。当然,您需要保留一个或多个分配器。

你能做的最好的事情就是了解你的问题,并根据程序中的模式和实际使用情况为分配器创建策略。通用malloc非常擅长它所做的事情,所以总是把它作为一个基线来衡量。如果您不知道自己的使用模式,您的分配器可能会比malloc慢。

还要记住,除非您为标准容器使用全局或线程本地和自定义分配器,否则您使用的这些类型将失去与标准容器的兼容性——这在许多情况下很快就会失效。另一种选择是编写自己的分配器和容器。

多个分配器的一些用途包括减少CPU使用、减少碎片和减少缓存未命中。因此,解决方案实际上取决于您的分配瓶颈是什么类型和在哪里

通过为活动线程提供无锁定堆,消除同步,可以提高CPU使用率。这可以在具有线程本地存储的内存分配器中完成。

通过从不同的堆中分配具有不同寿命的分配,碎片化将得到改善——将后台IO从用户的活动任务中分配到一个单独的堆中,将确保两者不会混淆。这很可能是通过为堆创建一个堆栈来实现的,当您处于不同的功能范围时,可以进行推送/弹出操作。

缓存未命中将通过将系统内的分配保持在一起而得到改善。四叉树/八叉树分配来自它们自己的堆将保证视图截头查询中存在局部性。最好通过重载特定类的操作符new和操作符delete(OctreeNode)来实现这一点。