【问题标题】:Should I pass allocator as a function parameter? (my misunderstanding about allocator)我应该将分配器作为函数参数传递吗? (我对分配器的误解)
【发布时间】:2017-06-14 17:58:39
【问题描述】:

在我通过阅读一些文章研究了几天allocator之后
(cppreferenceAre we out of memory) ,
我对如何控制数据结构以某种方式分配内存感到困惑。

我很确定我误解了一些东西,
所以我会把剩下的问题分成许多部分,让我的错误更容易被引用。

这是我(错误)理解的:-

片段

假设B::generateCs() 是一个从CPrototype 列表中生成C 列表的函数。
B::generateCs() 用于B() 构造函数:-

class C          {/*some trivial code*/};
class CPrototype {/*some trivial code*/};
class B {
    public: 
    std::vector<C> generateCs() {  
        std::vector<CPrototype> prototypes = getPrototypes();
        std::vector<C> result;                     //#X
        for(std::size_t n=0; n < prototypes.size(); n++) {
            //construct real object  (CPrototype->C)
            result.push_back( makeItBorn(prototypes[n]) ); 
        }
        return result;
    }
    std::vector<C> bField;    //#Y
    B() {
        this->bField = generateCs();    //#Y  ; "generateCs()" is called only here
    }
    //.... other function, e.g. "makeItBorn()" and "getPrototypes()"
};

从上面的代码来看,std::vector&lt;C&gt; 当前使用了一个通用的默认std::allocator

为简单起见,从现在开始,假设只有 2 个分配器(在 std::allocator 旁边),
我可以自己编码或从somewhere修改 :-

  • 堆分配器
  • 堆栈分配器

第 1 部分 (#X)

可以使用特定类型的分配器来改进这个 sn-p。
它可以在 2 个位置进行改进。 (#X#Y

std::vector&lt;C&gt;#X 行似乎是一个堆栈变量,
所以我应该使用stack allocator :-

std::vector<C,StackAllocator> result;   //#X

这往往会带来性能提升。 (#X 已完结。)

第 2 部分 (#Y)

接下来,更难的部分是B() 构造函数。 (#Y)
如果变量bField 具有适当的分配协议,那就太好了。

仅仅对调用者进行编码以显式使用分配器是无法实现的, 因为构造函数的调用者只能做到最好:-

std::allocator<B> bAllo;   
B* b = bAllo.allocate(1);   

bField的分配协议没有任何影响。

因此,构造函数本身有责任选择正确的分配协议。

第 3 部分

我不知道B 的实例将被构造为堆变量还是堆栈变量。
这很重要,因为此信息对于选择正确的分配器/协议很重要。

如果我知道它是哪一个(堆或堆栈),我可以将bField 的声明更改为:-

std::vector<C,StackAllocator> bField;     //.... or ....
std::vector<C,HeapAllocator> bField;     

不幸的是,由于信息有限(我不知道它会是堆/堆栈,它可以是两者),
这条路径(使用std::vector)导致了死胡同。

第 4 部分

因此,更好的方法是将分配器传递给构造函数:-

MyVector<C> bField; //create my own "MyVector" that act almost like "std::vector"
B(Allocator* allo) {
    this->bField.setAllocationProtocol(allo);  //<-- run-time flexibility 
    this->bField = generateCs();   
}

这很乏味,因为调用者必须将分配器作为附加参数传递,
但没有其他方法。

此外,当有许多调用者时,这是获得以下数据一致性优势的唯一实用方法,每个调用者都使用自己的内存块:-

class System1 {
    Allocator* heapForSystem1;
    void test(){
        B b=B(heapForSystem1);
    }
};
class System2 {
    Allocator* heapForSystem2;
    void test(){
        B b=B(heapForSystem2);
    }
};

问题

  • 我是从哪里开始出错的,怎么回事?
  • 如何改进 sn-p 以使用适当的分配器(#X#Y)?
  • 什么时候应该将分配器作为参数传递?

很难找到使用分配器的实际示例。

编辑(回复 Walter)

... 使用 std:allocator 以外的其他方法很少推荐。

对我来说,这是沃尔特回答的核心。
如果它是可靠的,这将是一个有价值的知识。

1.是否有任何书籍/链接/参考/证据支持它?
该列表不支持该声明。 (它实际上有点支持相反的情况。)
是亲身经历吗?

2。答案在某种程度上与许多来源相矛盾。请防御。
有许多来源建议不要使用std:allocator&lt;&gt;

更具体地说,它们只是一种在现实世界中很少值得使用的“炒作”吗?

另一个小问题:-
是否可以将声明扩展到“大多数优质游戏很少使用自定义分配器”?

3.如果我遇到这种罕见的情况,我必须付出代价,对吧?

只有两个好方法:-

  • 将分配器作为模板参数传递,或者
  • 作为函数(包括构造函数)的参数
  • (另一种不好的方法是创建一些关于使用什么协议的全局标志)

对吗?

【问题讨论】:

  • 好的,我会更加努力让它更容易阅读。
  • std::vector&lt;T&gt; size() 方法返回 std::vector&lt;T&gt;::size_type (aka std::size_t) size() 描述: cplusplus.com/reference/vector/vector/size 。我们什么时候应该使用 std::size_t: stackoverflow.com/questions/1951519/when-should-i-use-stdsize-t
  • @javaLoverit 不能保证,但可以stackoverflow.com/questions/3901630/…
  • @Inline Awww,明白,但我还有另一个个人原因——我不想为了这个小小的性能而牺牲这个小小的可读性(减少 1 行)。
  • result 被移动到generateCs 的返回值,而generateCs 又被移动到bField。 C++11 和标准的后续版本保证了这一点。填充 result 后,将不再进行复制或分配/解除分配。

标签: c++ c++11 memory memory-management allocation


【解决方案1】:

听起来您误解了堆栈分配器是什么。堆栈分配器只是使用堆栈(数据结构)的分配器。堆栈分配器可以管理在堆栈或堆上分配的内存。如果您不知道自己在做什么,那么使用它是很危险的,因为堆栈分配器在调用 deallocate 时会释放指定指针之后的所有内存。当数据结构中最近初始化的元素始终是下一个被销毁的元素时(或者最终一次将它们全部销毁),您可以使用堆栈分配器。

您可以查看一些 std 集合,了解它们如何允许程序员提供指定的分配器,例如 std::vector。它们使用可选的模板参数,因此用户可以选择分配器类。如果需要,它还允许您将分配器作为实例传递。如果不这样做,它会使用默认构造函数实例化一个。如果您不选择分配器类,则它使用仅使用堆的默认分配器。你也可以这样做。

template<typename C, typename Allocator = std::allocator<C> >
class B {

   vector<C, Allocator> bField;

   void generateCs() {  
     std::vector<CPrototype> prototypes = getPrototypes();
     for(std::size_t n=0; n < prototypes.size(); n++) {
         //construct real object  (CPrototype->C)
         bField.push_back( makeItBorn(prototypes[n]) ); 
     }
   }

   B(const Allocator& allo = Allocator()) : bField(allo) {
       generateCs();
   }
}

这允许用户在他们想要的时候控制分配,但如果他们不在乎,他们也会忽略它

【讨论】:

  • 粗略地说,您认为第 4 部分关于将分配器作为参数传递是正确的,对吧?这很乏味,但这并没有错。 (?) 需要强调的是,B 是一个绝对正常的类,但它的构造函数有“allocator”作为参数——这段代码并不难闻。 .... 使用这种逻辑,每个具有自定义数组之类的中等大小的类都应该有分配器作为参数。 (??)
  • 当然,你可以这样做,但你真的应该使用 const 引用而不是指针。我还将使用分配器而不是使用 setAllocationProtocol 来初始化初始化列表中的 bField(我以前没有使用过)。如果您担心繁琐,那么只需将其设为可选,以便仅在程序员愿意使用时才使用它。
  • 难以置信。你能提供一些关于这个的参考/链接/书吗?这是一个好习惯吗?
  • @javaLover 在初始化列表中初始化是一个很好的做法,在其他情况下,您分配给预初始化值。 stackoverflow.com/questions/9903248/…
  • @Inline 我的意思是很难相信“将分配器传递给构造函数”是一个好习惯。它会起作用,但听起来不对,所以我请求一些参考。我并不是说初始化器 : bField(allo) 看起来很奇怪。
【解决方案2】:

在 C++ 中,用于标准容器的分配器与容器类型相关联(见下文)。因此,如果您想控制类的分配行为(包括其容器成员),分配器必须是类型的一部分,即您必须将其作为template 参数传递:

template<template <typename T> Allocator>
class B
{
public:
  using allocator = Allocator<C>
  using fieldcontainer = std::vector<C,allocator>;
  B(allocator alloc=allocator{})
  : bFields(create_fields(alloc)) {}
private:
  const fieldcontainer bFields;
  static fieldcontainer create_fields(allocator);
};

但是请注意,有一个实验性的polymorphic allocator support,它允许您独立于类型更改分配器的行为。这当然比设计自己的MyVector&lt;&gt; 模板更可取。

请注意,仅在有充分理由的情况下才建议使用 std::allocator&lt;&gt; 以外的其他名称。可能的情况如下。

  1. 对于频繁分配和取消分配的小对象,可能首选堆栈分配器,但即使是堆分配器也可能不会降低效率。

  2. 提供内存对齐的分配器,例如 64 字节(适合对齐加载到 AVX 寄存器中)。

  3. cache-aligned allocator 有助于避免多线程情况下的错误共享。

  4. 分配器可以avoid default initialising trivially constructible 对象来提高多线程设置中的性能。


针对其他问题添加的注释。

文章 Are we out of memory 发表于 2008 年,不适用于当代 C++ 实践(使用 C++11 标准或更高版本),当内存管理使用 std 容器和智能指针(std::unique_ptr 和 @987654326 @) 避免了内存泄漏,这是写得不好的代码中内存需求增加的主要来源。

在为某些特定应用程序编写代码时,可能有充分的理由使用自定义分配器——C++ 标准库支持这一点,因此这是一种合法且适当的方法。充分的理由包括上面已经列出的那些,特别是在多线程环境中需要高性能或通过 SIMD 指令实现高性能时。

如果内存非常有限(可能在某些游戏机上),自定义分配器无法真正神奇地增加内存量。所以在这种情况下,分配器的使用,而不是分配器本身,是最关键的。不过,自定义分配器可能有助于减少内存碎片。

【讨论】:

    猜你喜欢
    • 2021-08-26
    • 1970-01-01
    • 2017-07-23
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2015-04-12
    • 2011-02-05
    相关资源
    最近更新 更多