为什么std::allocator::deallocate需要一个大小参数?

19

std::allocator是基于底层内存模型的抽象,封装了调用newdelete的功能。 delete没有大小要求,但deallocate()需要传入大小。

void deallocate( T* p, std::size_t n );
"参数 n 必须等于最初生成 p 的 allocate() 调用的第一个参数;否则行为未定义。"

为什么呢?

现在我要在释放内存之前进行额外的计算,或者开始存储我的分配大小。如果我没有使用allocator,我就不必这样做。


有一个趋势是明确提供大小,因为这会导致更好的优化和更快的堆代码。大多数情况下,编译器在调用delete时知道它。我记得这是从一些关于更改分配器内容的Going Native或Boostcon演讲中得出的结论。 - JDługosz
@JDługosz 编译器并不知道,C库中的free函数的实现知道,C++库中的delete []函数也是独立地知道的。 - Kuba hasn't forgotten Monica
@KubaOber 请参阅n3778。“当有可用的大小版本时,编译器应优先调用大小版本而不是未指定大小的版本。” 因此,编译器确实知道它,并且正如我所说,这可以为内存管理器节省查找指针的工作量。 像operator delete一样,分配器遵循这个新原则。 如果您不相信或需要详细解释原因,请查找演示文稿。 - JDługosz
编译器只知道要删除的实例的类型的大小。如果它与类型在给定位置最初分配的大小相同,那么它将起作用。如果该类型发生了变化,例如由于就地析构函数和放置 new,那么使用有大小限制的删除操作会导致未定义行为 :( 当然,这并不是每天都会遇到的代码,但是有大小限制的删除操作迫使你重新分配内存,每当对象的类型发生变化时...我不确定我是否喜欢它。我很想看到显示其好处的分配器基准测试。我有一段通过就地更改类型实现更快速的代码。 - Kuba hasn't forgotten Monica
分配器的用户知道大小,但我不会让编译器知道大小。编译器知道已删除类型的大小,并假定它与最初分配的类型的大小相同。这种假设可能不成立,因此似乎将新的未定义行为引入标准中...或者,我们现在必须注意在我们的代码中维护该不变量。 - Kuba hasn't forgotten Monica
5个回答

24
std::allocator API的设计 - Allocator概念 - 的目的是为了方便潜在的替代者。

std::allocator是基础内存模型的抽象。

它不必如此!通常情况下,分配器不需要使用C的mallocfree,也不需要delete或非就地的new。是的,默认的分配器通常会这样做,但分配器机制并不仅仅是C内存模型的抽象。成为不同的东西往往是自定义分配器的全部目的。请记住,分配器是可替换的:特定的std::allocator可能不需要释放的大小,但任何替代品都可能需要。 std::allocator的符合实现可以自由断言您确实将正确的n传递给deallocate,并且否则依赖于大小正确。

有时候mallocfree在其数据结构中存储块大小。但通常情况下,分配器可能不会这样做,要求它这样做是过早的悲观化。假设您有一个自定义池分配器,并且正在分配int的块。在典型的64位系统上,存储64位size_t以及32位int将导致200%的开销。分配器的用户更有优势,可以在分配时将大小一起存储,或以更便宜的方式确定大小。

优秀的malloc实现不会为每个小内存分配都存储分配大小; 它们能够从指针本身中推导出块大小,例如通过从块指针派生块指针,然后检查块头以获取块大小。当然这只是一个细节。您可以使用特定于平台的API获得最小大小,例如OS X上的malloc_size,Windows上的_msize,以及Linux上的malloc_usable_size


1
通常,内存分配算法尽量减少开销。一些跟踪空闲区域而不是已分配区域的算法可以将总开销降至低常数值,并且每个块的开销为零(记账信息完全存储在空闲区域中)。在使用这种算法的系统上,分配请求会从空闲池中移除存储,而释放请求会将存储添加到空闲池中。
如果使用池的连续区域满足了256和768字节的分配请求,则内存管理器状态与使用该相同区域满足两个512字节请求时的状态相同。如果内存管理器收到指向第一个块的指针并要求释放它,则无法知道第一个请求是256字节、512字节还是其他任何字节数,因此无法确定应该将多少内存添加回池中。
在这样的系统上实现"malloc"和"free"需要在存储区域的开头存储每个块的长度,并返回指向该长度后下一个适当对齐地址的指针。虽然实现这一点是可能的,但它会为每个分配添加4-8字节的开销。如果调用者可以告诉释放例程要将多少存储空间添加回内存池,则可以消除此类开销。

0
此外,很容易满足这个设计要点:只需将事物分配为一个struct,并将大小存储为该struct中的一个元素。调用解分配器的代码现在知道要提供什么值,因为结构本身包含它。
通过这样做,你实际上正在做任何其他语言实现可能会慷慨地为你做的事情。你只是明确地做同样的事情。
现在,考虑到我们谈论的是已经内置了大量优秀容器类的C++,我坦率地鼓励你尽可能避免“自己动手”。只需找到一种使用语言和标准库已经提供的精巧容器类的方法即可。
否则,请确保将您在此处构建的内容打包为“自制容器类”。确保处理分配器和解除分配器的逻辑仅在程序中发生一次。(即在此类内部。)大量使用专门设计用于检测漏洞的逻辑进行精心推散。(例如,在分配对象时插入到对象中的哨兵值,在释放对象时必须找到它,并在其之前被清除。显式检查存储的大小值,以确保其合理性。依此类推。)

-1
你不需要跟踪大小。标准分配器不跟踪大小,因为它假定所有分配都有一个大小。当然,不同类型的分配器用于不同的目的。像视频游戏这样的应用程序会预先分配所有内存,并消除需要为每个分配跟踪大小的开销。

标准库试图尽可能地通用。有些分配器需要跟踪大小,而有些则不需要,但所有分配器都需要符合接口规范。


这是错误的:是的,任何分配器的用户都需要跟踪大小,因为分配器旨在可替换。std::allocatorAllocator 概念 的一个实现。所有这些实现都要求将正确的 n 传递给 deallocate!它们可以选择忽略它,但是 用户 应该提供它,因为使用 n 是一个实现细节。如果愿意,用户当然可以将大小存储在分配本身中。 - Kuba hasn't forgotten Monica
@KubaOber,您似乎误解了我的回答。如果n是固定大小,则无需跟踪它。这是一种实现细节。标准不可能预测所有分配器的实现方式。因此,接口是严格的。 - user6678809
只要传递正确的值,你如何定义 n 都无所谓。当然,你不需要为 n 显式地分配存储空间,但如果你说“你不需要跟踪 n”,那么你就是愚蠢的。是的,你需要跟踪它,但不一定要使用变量或任何其他运行时存储。典型的使用 malloc 的标准分配器不会为所有分配指定任何大小:你需要向 allocate 传递一个正确的 n。如果它使用 free 而不是 delete[],它将需要一个正确的 n 来调用析构函数! - Kuba hasn't forgotten Monica
抱歉,当然deallocate不会调用析构函数。 - Kuba hasn't forgotten Monica

-4

我没有确凿的证据,但我的直觉是,分配器不需要使用C++的运算符new/delete,而可以使用没有分配数组并知道其大小能力的内存管理例程 - 例如malloc。


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