我应该将分配器作为函数参数传递吗? (我对分配器的误解)
Should I pass allocator as a function parameter? (my misunderstanding about allocator)
我通过阅读一些文章研究了allocator
几天后
(cppreference and Are 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<C>
当前使用通用默认值 std::allocator
。
为简单起见,从现在开始,假设只有 2 个分配器(除了 std::allocator
),
我可以自己编码或修改 somewhere
:-
- 堆分配器
- 堆栈分配器
第 1 部分 (#X)
可以使用特定类型分配器改进此代码段。
它可以在 2 个位置进行改进。 (#X
和 #Y
)
std::vector<C>
在第 #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
的一个实例会被构造成堆变量还是栈变量
这很重要,因为此信息对于选择正确的 allocator/protocol 很重要。
如果我知道它是哪一个(堆或堆栈),我可以将 bField
的声明更改为:-
std::vector<C,StackAllocator> bField; //.... or ....
std::vector<C,HeapAllocator> bField;
可惜信息有限(我不知道会是哪个heap/stack,两者都可以),
这条路(使用 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);
}
};
问题
- 我哪里开始出错了,怎么办?
- 如何改进代码段以使用适当的分配器(
#X
和 #Y
)?
- 什么时候应该将分配器作为参数传递?
很难找到有关使用分配器的实际示例。
编辑(回复Walter)
... using another than std:allocator<> is only rarely recommendable.
对我来说,这是沃尔特回答的核心。
如果它是可靠的,那将是一个有价值的知识。
1.有没有book/link/reference/evidence支持的?
该列表不支持该声明。 (它实际上支持相反的一点。)
是亲身经历吗?
2。答案在某种程度上与许多来源相矛盾。请防御
有很多来源建议不要使用 std:allocator<>
。
- Are we out of memory :
不能回答"How much memory are you using for subsystem X?"是有罪的
- Custom C++ allocators suitable for video games
这意味着自定义分配器是控制台游戏的必需品。
@节"Why replace the default allocator?"
- Memory Management part 1 of 3
没有自定义分配器 = "every now and then there’s a little lag (in game)"
更具体地说,它们只是 "hype" 在现实世界中很少值得使用的东西吗?
另一个小问题:-
索赔可以扩大到"Most quality games rarely use custom allocator"吗?
3。如果我遇到这种罕见的情况,我必须支付费用,对吗?
只有两个好方法:-
- 将分配器作为模板参数传递,或者
- 作为函数(包括构造函数)的参数
- (另一个糟糕的方法是创建一些关于使用什么协议的全局标志)
是否正确?
听起来你误解了什么是堆栈分配器。堆栈分配器只是一个使用堆栈(数据结构)的分配器。堆栈分配器可以管理在堆栈或堆上分配的内存。如果您不知道自己在做什么,那么使用它是很危险的,因为调用 deallocate 时,堆栈分配器会释放指定指针之后的所有内存。当数据结构中最近初始化的元素始终是下一个被销毁的元素时(或者如果您最终一次全部销毁它们),您可以使用堆栈分配器。
您可以查看一些标准集合,了解它们如何允许程序员提供指定的分配器,例如 std::vector。它们使用可选的模板参数,因此用户可以选择分配器 class。如果需要,它还允许您将分配器作为实例传递。如果不这样做,它会使用默认构造函数实例化一个。如果您不选择分配器 class,那么它将使用仅使用堆的默认分配器。你也可以这样做。
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();
}
}
这允许用户在他们想要的时候控制分配,但如果他们不在乎,他们也会忽略它
在 C++ 中,用于标准容器的分配器与容器类型相关(但请参见下文)。因此,如果您想控制 class(包括其容器成员)的分配行为,分配器必须是类型的一部分,即您必须将其作为 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<>
模板更可取。
请注意,只有在有充分理由的情况下才建议使用 std::allocator<>
以外的其他方法。可能的情况如下。
对于频繁分配的小对象和de-allocated,堆栈分配器可能是首选,但即使是堆分配器也可能效率不低。
提供内存对齐的分配器,比如说,64 字节(适用于对齐加载到 AVX 寄存器)。
A cache-aligned allocator 可用于在 multi-threaded 情况下避免虚假共享。
分配器可以 avoid default initialising trivially constructible 对象来提高 multi-threaded 设置中的性能。
为回答其他问题而添加的注释。
文章 Are we out of memory dates from 2008 and doesn't apply to contemporary C++ practice (using the C++11 standard or later), when memory management using std
containers and smart pointers (std::unique_ptr
and std::shared_ptr
) 避免了内存泄漏,内存泄漏是编写糟糕的代码时增加内存需求的主要原因。
在为某些特定应用程序编写代码时,可能有很好的理由使用自定义分配器——C++ 标准库支持这一点,因此这是一种合法且适当的方法。充分的理由包括上面已经列出的那些,特别是当在 multi-threaded 环境中需要高性能或通过 SIMD 指令实现时。
如果内存非常有限(在某些游戏机上可能如此),自定义分配器无法真正神奇地增加内存量。所以在这种情况下,最关键的是分配器的使用,而不是分配器本身。不过,自定义分配器可能有助于减少内存碎片。
我通过阅读一些文章研究了allocator
几天后
(cppreference and Are 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<C>
当前使用通用默认值 std::allocator
。
为简单起见,从现在开始,假设只有 2 个分配器(除了 std::allocator
),
我可以自己编码或修改 somewhere
:-
- 堆分配器
- 堆栈分配器
第 1 部分 (#X)
可以使用特定类型分配器改进此代码段。
它可以在 2 个位置进行改进。 (#X
和 #Y
)
std::vector<C>
在第 #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
的一个实例会被构造成堆变量还是栈变量
这很重要,因为此信息对于选择正确的 allocator/protocol 很重要。
如果我知道它是哪一个(堆或堆栈),我可以将 bField
的声明更改为:-
std::vector<C,StackAllocator> bField; //.... or ....
std::vector<C,HeapAllocator> bField;
可惜信息有限(我不知道会是哪个heap/stack,两者都可以),
这条路(使用 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);
}
};
问题
- 我哪里开始出错了,怎么办?
- 如何改进代码段以使用适当的分配器(
#X
和#Y
)? - 什么时候应该将分配器作为参数传递?
很难找到有关使用分配器的实际示例。
编辑(回复Walter)
... using another than std:allocator<> is only rarely recommendable.
对我来说,这是沃尔特回答的核心。
如果它是可靠的,那将是一个有价值的知识。
1.有没有book/link/reference/evidence支持的?
该列表不支持该声明。 (它实际上支持相反的一点。)
是亲身经历吗?
2。答案在某种程度上与许多来源相矛盾。请防御
有很多来源建议不要使用 std:allocator<>
。
- Are we out of memory :
不能回答"How much memory are you using for subsystem X?"是有罪的 - Custom C++ allocators suitable for video games
这意味着自定义分配器是控制台游戏的必需品。
@节"Why replace the default allocator?" - Memory Management part 1 of 3
没有自定义分配器 = "every now and then there’s a little lag (in game)"
更具体地说,它们只是 "hype" 在现实世界中很少值得使用的东西吗?
另一个小问题:-
索赔可以扩大到"Most quality games rarely use custom allocator"吗?
3。如果我遇到这种罕见的情况,我必须支付费用,对吗?
只有两个好方法:-
- 将分配器作为模板参数传递,或者
- 作为函数(包括构造函数)的参数
- (另一个糟糕的方法是创建一些关于使用什么协议的全局标志)
是否正确?
听起来你误解了什么是堆栈分配器。堆栈分配器只是一个使用堆栈(数据结构)的分配器。堆栈分配器可以管理在堆栈或堆上分配的内存。如果您不知道自己在做什么,那么使用它是很危险的,因为调用 deallocate 时,堆栈分配器会释放指定指针之后的所有内存。当数据结构中最近初始化的元素始终是下一个被销毁的元素时(或者如果您最终一次全部销毁它们),您可以使用堆栈分配器。
您可以查看一些标准集合,了解它们如何允许程序员提供指定的分配器,例如 std::vector。它们使用可选的模板参数,因此用户可以选择分配器 class。如果需要,它还允许您将分配器作为实例传递。如果不这样做,它会使用默认构造函数实例化一个。如果您不选择分配器 class,那么它将使用仅使用堆的默认分配器。你也可以这样做。
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();
}
}
这允许用户在他们想要的时候控制分配,但如果他们不在乎,他们也会忽略它
在 C++ 中,用于标准容器的分配器与容器类型相关(但请参见下文)。因此,如果您想控制 class(包括其容器成员)的分配行为,分配器必须是类型的一部分,即您必须将其作为 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<>
模板更可取。
请注意,只有在有充分理由的情况下才建议使用 std::allocator<>
以外的其他方法。可能的情况如下。
对于频繁分配的小对象和de-allocated,堆栈分配器可能是首选,但即使是堆分配器也可能效率不低。
提供内存对齐的分配器,比如说,64 字节(适用于对齐加载到 AVX 寄存器)。
A cache-aligned allocator 可用于在 multi-threaded 情况下避免虚假共享。
分配器可以 avoid default initialising trivially constructible 对象来提高 multi-threaded 设置中的性能。
为回答其他问题而添加的注释。
文章 Are we out of memory dates from 2008 and doesn't apply to contemporary C++ practice (using the C++11 standard or later), when memory management using std
containers and smart pointers (std::unique_ptr
and std::shared_ptr
) 避免了内存泄漏,内存泄漏是编写糟糕的代码时增加内存需求的主要原因。
在为某些特定应用程序编写代码时,可能有很好的理由使用自定义分配器——C++ 标准库支持这一点,因此这是一种合法且适当的方法。充分的理由包括上面已经列出的那些,特别是当在 multi-threaded 环境中需要高性能或通过 SIMD 指令实现时。
如果内存非常有限(在某些游戏机上可能如此),自定义分配器无法真正神奇地增加内存量。所以在这种情况下,最关键的是分配器的使用,而不是分配器本身。不过,自定义分配器可能有助于减少内存碎片。