std::vector *have* 在增加容量时移动对象吗?或者,分配器可以“重新分配”吗?
Posted
技术标签:
【中文标题】std::vector *have* 在增加容量时移动对象吗?或者,分配器可以“重新分配”吗?【英文标题】:Does std::vector *have* to move objects when growing capacity? Or, can allocators "reallocate"? 【发布时间】:2011-12-21 15:21:36 【问题描述】:different question 激发了以下想法:
std::vector<T>
有在增加容量时移动所有元素吗?
据我了解,标准行为是底层分配器请求新大小的整个块,然后移动所有旧元素,然后销毁旧元素,然后释放旧内存。
考虑到标准分配器接口,这种行为似乎是唯一可能的正确解决方案。但我想知道,修改分配器以提供reallocate(std::size_t)
函数是否有意义,该函数将返回pair<pointer, bool>
并可以映射到底层realloc()
?这样做的好处是,如果操作系统实际上可以扩展分配的内存,那么根本不需要移动。布尔值将指示内存是否已移动。
(std::realloc()
可能不是最好的选择,因为如果我们不能扩展,我们不需要复制数据。所以实际上我们宁愿想要像extend_or_malloc_new()
这样的东西。编辑: 也许基于is_pod
-trait 的特化可以让我们使用实际的realloc
,包括它的按位复制。只是一般情况下不会。)
这似乎是一个错失的机会。最坏的情况是,您始终可以将reallocate(size_t n)
实现为return make_pair(allocate(n), true);
,这样就不会受到任何惩罚。
是否有任何问题导致此功能不适合或不适合 C++?
也许可以利用这一点的唯一容器是std::vector
,但话说回来,这是一个相当有用的容器。
更新:一个小例子来澄清。当前resize()
:
pointer p = alloc.allocate(new_size);
for (size_t i = 0; i != old_size; ++i)
alloc.construct(p + i, T(std::move(buf[i])))
alloc.destroy(buf[i]);
for (size_t i = old_size; i < new_size; ++i)
alloc.construct(p + i, T());
alloc.deallocate(buf);
buf = p;
新实现:
pair<pointer, bool> pp = alloc.reallocate(buf, new_size);
if (pp.second) /* as before */
else /* only construct new elements */
【问题讨论】:
我认为它不需要一对,您可以简单地与传入的指针进行比较。只要重新分配理解正确的移动语义我想不出问题。 @MooingDuck:关于您的第一条评论:唯一的可能是分配器的 grow 函数在无法grow,并让内存保持原样(没有按位复制)。当你开始比较realloc
的指针时,损坏已经完成。
@David: grow
可以说是一个更好的功能名称!
@Praetorian:按位复制有不同的问题......例如,考虑可能存在内部指针,例如我使用了 NullObject
模式的实现,其中对象持有一个 null-object 和一个指向当前对象的指针,它可以引用动态分配的 real-object 或 null-object 成员.在对象为 null 的情况下,指针引用同一对象的另一个成员。在这种情况下,按位复制会留下悬空指针。
std::deque
是最不幸的容器之一。它真的很擅长它的工作。而且您几乎从不需要它的功能。对于 std::container,几何增长的循环缓冲区比 std::deque 更好。循环缓冲区具有更好的性能和更少的复杂性。但它不能保证像std::deque
和std::list
这样的引用的稳定性。但根据我的经验,循环缓冲区比 std::deque 更好地解决了大多数 push-pop 队列问题,如果没有,std::list
是正确的选择。
【参考方案1】:
当std::vector<T>
容量不足时,它必须分配一个新块。你已经正确地解释了原因。
IMO 它会增加分配器接口是有意义的。我们两个人尝试过 C++11,但我们无法获得支持:[1][2]
我开始确信,为了完成这项工作,需要一个额外的 C 级 API。我也未能获得支持:[3]
【讨论】:
太好了,谢谢!很遗憾听到您未能获得进一步的支持——添加 C 接口函数似乎完全是微不足道的:只需使用realloc()
并删除复制内存的部分。它甚至不必成为 C 标准的一部分。它可能只是 C++ 库实现的东西......
自定义 C++ 分配器将受益于 C realloc-but-don't-move
函数。这是带着这个去 C 委员会的主要动机。
好吧,other,用户定义的类总是有可能使用具有这种“可能增长”接口的分配器。可惜被拒绝了。但是,编写一个比规定的分配器接口更多的分配器是否可以?
是的。但是std::containers
不够聪明,无法使用您的扩展界面。但也许 boost::intrusive 容器会。该库的作者与编写 N2045 的人是同一个人。
@gnzlbg - 你实际上不需要为每个mmap
维护一个指针来释放它。我猜你的假设是你需要将你从mmap
(如new
和delete
)获得的相同指针传递回munmap
,但它不起作用:你可以传递整个您要删除的页面范围,即使它们来自不同的mmap
调用,它们也会被释放。如果其中一些根本没有映射,这甚至不是错误!所以你只需要指向区域开始的指针(已经是向量的一部分)和容量(已经是向量的一部分)......【参考方案2】:
在大多数情况下,realloc
不会扩展内存,而是分配一个单独的块并移动内容。最初定义 C++ 时考虑到了这一点,并决定当前接口更简单,并且在常见情况下效率不低。
在现实生活中,realloc
能够成长的情况很少。在malloc
具有不同池大小的任何实现中,新大小(请记住vector
大小必须以几何方式增长)可能会落在不同的池中。即使在没有从任何内存池分配的大块的情况下,它也只能在更大大小的虚拟地址空闲时才能增长。
请注意,虽然realloc
有时可以增长内存而不移动,但当realloc
完成时它可能已经移动 (按位移动)内存,并且二进制 move 将导致所有非 POD 类型的未定义行为。我不知道任何分配器实现(POSIX、*NIX、Windows),您可以在其中询问系统是否能够增长,但如果它需要移动。
【讨论】:
我认为他的意思是一个新的重新分配功能,与 malloc.hrealloc
不兼容,当无法调整大小时,它不会移动内容,也不会释放旧块。
另外,VirtualAlloc
和 mmap
允许您在块之后立即请求地址,因此您要么扩展块,要么失败。但我也不知道有任何基于堆的分配器支持“尽可能增长”操作。
@Ben:是的,确实如此。我需要 1) 为标准库分配器添加一个接口,以及 2) 一个底层实现,如果可能的话,它会增长,但如果没有,则执行裸 malloc
,而不需要移动。
@BenVoigt:显然这样的功能肯定已经存在:我可以撕开realloc
并杀死按位移动的部分,不是吗? realloc++
:-)
Linux 提供了mremap
,您可以使用它来扩展现有的匿名内存映射(或尝试失败)。【参考方案3】:
是的,你说得对,标准分配器接口没有为 memcpy'able 类型提供优化。
可以使用 boost 类型特征库来确定一个类型是否可以被 memcpy 使用(不确定他们是开箱即用地提供它,还是必须基于 boost 构建一个复合类型鉴别器)。
无论如何,要利用realloc()
,可能会创建一种可以显式利用此优化的新容器类型。使用当前的标准分配器接口,这似乎是不可能的。
【讨论】:
不,不,我不关心 memcpyability。我已经准备好调用我的分配器的构造和销毁序列。我只是想给分配器偷懒的机会!以上是关于std::vector *have* 在增加容量时移动对象吗?或者,分配器可以“重新分配”吗?的主要内容,如果未能解决你的问题,请参考以下文章
即使根据容量()仍有未使用的空间,std::vector 能否将其数据移动到 emplace_back()处的另一个地址?