高效使用多个内存分配器

9

我一直在研究如何将我的分配方法从简单重载新的方式转换为通过代码库使用多个分配器。但是,我如何有效地使用多个分配器呢?通过我的研究,我能想到的唯一办法是让这些分配器成为全局变量。然而,由于通常有很多全局变量的使用是一个“坏主意”,所以这似乎存在问题。

我希望找到如何有效使用多个分配器的方法。例如,我可能需要一个分配器只用于特定子系统,另一个分配器则用于不同的子系统。我不确定是否唯一的方法是使用多个全局分配器,因此我希望能获得更好的见解和设计。


4
为什么分配器必须是全局的?只要每个已分配单元都有一个引用指向自己的分配器以便正确释放,分配器实际上在哪里似乎并不重要。 - Martin James
分配器会为已分配的单元留下什么位置呢?在我看来,它似乎必须是全局的。 - chadb
3个回答

11

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::string来创建std::string。这在先前的C++2011分配器中不会发生。
要在子系统中实际使用分配器,您需要有效地传递它们,或者明确作为参数传递给您的函数和/或类,或者通过支持分配器的对象隐式传递,作为上下文。例如,如果您将任何标准容器用作[部分]传递的上下文,则可以使用其get_allocator() 方法获取所使用的分配器。

非常有趣,我之前没有考虑过引用计数。既然我将创建自己的分配器,那么我是否应该让它继承自 weak_reference 等类以允许引用计数? - chadb
1
个人而言,我会将 std::shared_ptr<my_allocation_base> 作为分配器 my_allocator 的成员。实际的分配逻辑将位于从 my_allocation_base 派生的类中。如果您只有一种分配方法,当然可以直接将逻辑放入指向的对象中。使用 weak_reference(不知道这是什么)听起来好像行不通:您希望每个仍然持有相应分配内存的对象都有一个对分配对象的实际引用,并且在释放后减少引用计数。 - Dietmar Kühl

3
您可以使用new放置。这既可以用于指定内存区域,也可以用于重载类型的static void* operator new(ARGS)。全局变量在这里不是必需的,而且如果效率很重要并且您的问题很苛刻,那么这真的是一个坏主意。当然,您需要保留一个或多个分配器。
最好的方法是了解您的问题,并根据程序中的模式和实际使用情况创建分配器策略。通用的malloc非常擅长它所做的事情,因此始终将其用作一条基线来进行测量。如果您不知道自己的使用模式,则您的分配器可能会比malloc慢。
还要记住,除非您为标准容器使用全局或线程本地和自定义分配器,否则您使用的这些类型将失去与标准容器的兼容性——这在许多情况下很快就会失去意义。另一种选择是编写自己的分配器和容器。

分配器应该如何持有?你提到它们不应该是全局的,但它们应该在哪里呢?(另外,我不关心与标准容器的兼容性,因为我已经有了自己的容器)。 - chadb
@chadb 这取决于问题。我同时使用成员引用(例如,使用引用计数分配)和外部引用(例如,一个分配器用于图形,其节点都由一个分配器管理)。线程本地分配器(由线程或其数据访问)是另一种方法,尽管在这种情况下我更喜欢外部引用。 - justin
那么针对整个子系统使用的分配器(其根不在单例中)呢?这似乎是我目前的主要用例,正如我在原始帖子中提到的那样。那么,该分配器将存储在哪里?除了全局变量之外,我想不出任何情况。 - chadb
@chadb 我提到了图表 - 假设这是一个非平凡的例子:this->view()->rootView()->bitmapImageAllocator()。在这种情况下,分配器由根视图持有。您还可以隐藏我提到的默认 operator new,以强制执行特定的分配器,或将其作为构造函数参数传递。虽然不总是方便,但在需要自定义分配器的情况下,可以传递/保留这些引用。因此,对象可以保留对引用的控制,或者根据需要将它们作为参数传递。您还可以使它们成为线程本地的。 - justin
@chadb 图表只是一个例子。我提供的例子是,视图层次结构中的根视图可以持有分配器,而子视图及其成员可以通过层次结构引用它们(通过层次结构)。这种方法也可以应用于集合,这是一个更简单的情况。因此,请考虑如何实现可以引用比全局更局部的分配器。如果每个根视图都持有一个分配器,并且每个子视图都可以引用其父视图并访问由根视图持有的分配器,则它们可以访问您想要添加到该视图层次结构中的任何分配器。 (cont) - justin
显示剩余3条评论

1

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

通过为活动线程提供无锁堆来消除同步,可以改善CPU使用情况。这可以在内存分配器中使用线程本地存储来完成。

通过将具有不同生命周期的分配从不同的堆中分配来改善碎片化——将后台IO分配到与用户活动任务不同的堆中将确保两者不会相互干扰。这通常是通过为堆设置堆栈,在不同的功能作用域中进行推入/弹出来完成的。

通过将分配保持在系统内部来改善缓存未命中。将Quadtree/Octree分配来自它们自己的堆将确保视图 frustrum 查询中存在局部性。最好通过重载特定类(OctreeNode)的operator new和operator delete来完成。


也许有些误解,但我的问题主要是关于如何使用多个分配器,而不是为什么。 - chadb

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接