【问题标题】:Using multiple allocators efficiently有效地使用多个分配器
【发布时间】:2012-03-27 20:58:27
【问题描述】:

我一直在研究将我的分配方法从简单的重载新转换为通过代码库使用多个分配器。但是,如何有效地使用多个分配器?我可以通过我的研究设计的唯一方法是让分配器是全局的。虽然,这似乎有问题,因为使用许多全局变量通常是一个“坏主意”。

我正在寻找如何有效地使用多个分配器。例如,我可能有一个分配器仅用于特定子系统,而不同的分配器用于不同的子系统。我不确定这样做的唯一方法是否是通过使用多个全局分配器,所以我希望有更好的洞察力和设计。

【问题讨论】:

  • 为什么分配器必须是全局的?只要每个分配的单元都引用了它自己的分配器以便它可以被正确释放,那么分配器实际在哪里重要吗?
  • 分配器会为分配的单元离开哪里?在我看来,它必须是全球性的。

标签: c++ design-patterns memory memory-management architecture


【解决方案1】:

多个分配器的一些用途包括减少 CPU 使用率、减少碎片和减少缓存未命中。所以解决方案真的取决于你的分配瓶颈是什么类型以及在哪里。

CPU 使用率将通过为活动线程设置无锁堆来提高,从而消除同步。这可以在带有线程本地存储的内存分配器中完成。

通过从不同的堆分配具有不同生命周期的分配来改进碎片 - 将后台 IO 分配到与用户活动任务不同的堆中将确保两者不会相互混淆。这很可能是通过为您的堆设置一个堆栈,并在您处于不同的功能范围时推送/弹出来完成的。

通过将系统内的分配保持在一起,可以改善缓存未命中率。让 Quadtree/Octree 分配来自它们自己的堆将保证在视锥体查询中存在局部性。这最好通过重载特定类 (OctreeNode) 的 operator new 和 operator delete 来完成。

【讨论】:

  • 也许有些东西被误解了,但是,我的问题主要是如何使用多个分配器,而不是为什么。
【解决方案2】:

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

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

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

【讨论】:

  • 很有意思,没想到引用计数。既然我将创建自己的分配器,那么我是否应该让它从诸如 weak_reference 之类的东西继承以允许引用计数?
  • 就个人而言,我会使用std::shared_ptr<my_allocation_base> 作为分配器my_allocator 的成员。实际的分配逻辑将位于派生自my_allocation_base 的类中。如果您只有一种分配方法,当然可以将逻辑直接放入指向的对象中。使用weak_reference(不知道这是什么)听起来好像不起作用:您希望从每个仍然持有相应分配的内存的对象中获得对分配对象的实际引用,并且引用计数一旦被减少发布。
【解决方案3】:

您可以使用new 展示位置。这可用于指定内存区域,或重载类型的static void* operator new(ARGS)。如果效率很重要并且您的问题很苛刻,那么全局变量不是必需的,这在这里真的是个坏主意。当然,您需要保留一个或多个分配器。

您能做的最好的事情是了解您的问题并根据您的程序中的模式和实际使用情况为分配器创建策略。通用malloc 非常擅长它的功能,因此请始终将其用作衡量的基准。如果您不知道自己的使用模式,您的分配器可能会比malloc 慢。

另外请记住,您使用的这些类型将失去与标准容器的兼容性,除非您为标准容器使用全局或线程本地和自定义分配器——这在许多情况下很快就无法达到目的。另一种方法是编写自己的分配器和容器。

【讨论】:

  • 分配器应该如何持有?您提到它们不应该是全球性的,但是应该在哪里呢? (此外,我不关心与标准容器的兼容性,因为我已经拥有自己的容器)。
  • @chadb 这取决于问题。我同时使用成员引用(例如,使用引用计数分配)和外部引用(例如,其节点都由一个分配器管理的图的分配器)。线程本地分配器(由线程或其数据访问)是另一种方法,尽管在这种情况下我更喜欢外部引用。
  • 专门用于整个子系统(其根不在单例中)的分配器呢?正如我在原始帖子中提到的那样,这似乎是我目前的主要用例。分配器将存储在哪里?除了那里的全球,我想不出任何情况。
  • @chadb 我提到了图表——假设这个不平凡的例子:this->view()->rootView()->bitmapImageAllocator()。在这种情况下,分配器由根视图持有。您还可以隐藏我提到的默认 operator new 以强制执行特定的分配器,或者将其作为构造函数参数传递。这并不总是方便,但可以在自定义分配器很重要的地方传递/保留那些引用。因此对象可以保留引用,或者您可以根据需要将它们作为参数传递。你也可以让它们成为本地线程。
  • 如果我不使用“图表”怎么办(我实际上并不熟悉“图表”,它不适合我当前的架构)。线程问题是不使它们成为全球性的唯一原因吗?如果是这样的话,为什么不把特定的“分配”函数线程安全地放在函数的一侧?
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2023-02-03
  • 1970-01-01
  • 2014-08-08
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多