std::vector::push_back的代价成功或无效
Cost of std::vector::push_back either succeeding or having no effect?
如果我理解正确的话,在复制或移动期间抛出异常的情况下,std::vector::insert
不保证std::vector
s的提交或回滚(std::list
s的原因很明显),因为检查异常的成本很高。我最近看到push_back
确实保证在最后成功插入或什么都不发生。
push_pack
期间,向量必须调整大小(重新分配)。在这种情况下,必须通过复制或移动语义将所有元素复制到新容器中。假设移动构造函数不保证noexcept
,那么std::vector
将使用复制语义。因此,为了保证push_pack
的上述行为,std::vector
必须检查复制是否成功,如果没有,则通过交换初始向量进行回滚。这就是正在发生的事情吗?如果是这样,这不是很昂贵吗?或者,因为重新分配很少发生,所以可以说平摊代价很低?
在c++ 98/03中,我们(显然)没有移动语义,只有复制语义。而在c++ 98/03中,push_back
有很强的保障。c++ 11的一个强烈动机是不破坏依赖于这种强保证的现有代码。
- 如果
is_nothrow_move_constructible<T>::value
为真,移动,否则 - 如果
is_copy_constructible<T>::value
为真,复制,否则 - 如果
is_move_constructible<T>::value
为真,移动,否则 - 代码格式错误
如果我们在1或2的情况下,我们有强保证。如果是第三种情况,我们只有基本的保证。因为在c++ 98/03中,我们总是在情形2中,我们有向后兼容性。
Case 2不需要昂贵的维护强保证。一个是分配新的缓冲区,最好使用RAII设备,比如第二个向量。复制到其中,只有当所有这些都成功时,才能将*this
与RAII设备交换。这是最便宜的做事方式,不管你是否想要强有力的保证,你都可以免费得到它。
情形1维护强保证的成本也很低。我所知道的最好的方法是首先复制/移动新元素到新分配的中间。如果成功,则从旧缓冲区中移动元素并交换。
比你想要的更多的细节
libc++用相同的算法完成了这三种情况。为此,使用了两个工具:
-
std::move_if_noexcept
- 类似
vector
的非标准容器,其中数据是连续的,但可以从分配缓冲区开始的非零偏移处开始。libc++把这个东西叫做split_buffer
。
假设在重新分配的情况下(非重新分配的情况是微不足道的),split_buffer
是通过引用这个vector
的分配器来构建的,其容量是这个vector
的两倍,并且其起始位置设置为this->size()
(尽管split_buffer
仍然是empty()
)。
然后将新元素复制或移动(取决于我们谈论的是哪个push_back
重载)到split_buffer
。如果失败,split_buffer
析构函数撤销分配。如果成功,那么split_buffer
现在有size() == 1
,并且split_buffer
在其第一个元素之前正好有this->size()
个元素的空间。
接下来,元素以倒序从this
移动/复制到split_buffer
。move_if_noexcept
用于此,它的返回类型为T const&
或T&&
,这正是我们需要的,正如上面3个案例所指定的那样。在每次成功移动/复制时,split_buffer
都在执行push_front
。如果成功,split_buffer
现在有size() == this->size()+1
,并且它的第一个元素从它分配的缓冲区开始的偏移量为零。如果任何移动/复制失败,split_buffer
的析构函数将销毁split_buffer
中的所有内容并释放缓冲区。
接下来split_buffer
和this
交换它们的数据缓冲区。这是noexcept
的操作。
最后,split_buffer
销毁它的所有元素,并释放它的数据缓冲区。
不需要try-catch。没有额外的费用。一切都按照c++ 11的规定工作(如上所述)。
这就是正在发生的事情,它可能是昂贵的。我们决定,push_back的强保证比move语义的性能更重要。
重新分配是昂贵的,是的。这就是为什么重新分配的代价是通过多次调用std::vector::push_back
来平摊的。
最常见的是通过将旧大小乘以某个常数因子(例如1.5或2)来分配新的块大小,例如1,2,4,8,16,32,…
重新分配时,std::vector
分配新块,复制元素,销毁旧元素并释放旧块。如果失败(抛出异常),我们可以简单地中止复制过程,并开始销毁复制的元素——所以最坏情况下的代价是2*(n-1)+d
,其中d
是释放的代价(我们复制n-1
个元素,当复制第n个元素时,抛出异常,因此我们销毁n-1
个元素,并释放新块)
注意,以上适用于移动不为noexcept
的情况。如果移动是noexcept
,唯一可能的故障点是分配新的内存块(因为析构函数要求是noexcept
),重新分配过程更简单、更快。
您可以通过使用不同的容器(比如不存在问题的std::deque
)或事先使用std::vector::reserve
(这需要知道对元素计数的估计)来减少重新分配对性能的影响。
- cmake在我的项目中所需的所有静态库都不成功
- 尽管测试成功,CppUnit测试核心仍被丢弃.为什么
- 如何让LLDB在成功时退出,在失败时等待
- 有没有办法知道Tracer是否成功地完全连接到了jaegerclientcpp中的jaeger后端服务器
- CMake WxWidgets项目成功地在Linux上构建,但没有在Windows上构建
- 为什么 std::绑定错误参数可以成功?
- Clion显示错误,但可以使用Cmake成功构建代码
- 代码使用向量成功运行,但使用数组显示错误
- 如何检查cURL是否成功登录?c ++
- 为什么 WinInet 在通过 FQDN 连接时无法通过协商自动进行身份验证,但如果通过 IP 连接则成功?
- C++为什么我的编译器成功了,但我的计算机给出了调试错误?
- 未知的 GCC 链接器错误,但已成功构建
- 我的 SonarQube C++扫描成功,但结果仅标记重复项,而没有标记其他标记的位置
- 为什么 LoadLibrary 失败,而 LoadLibraryA 成功加载 DLL?
- 生成成功,但不会给出正确的输出
- 默认/样板代码在Visual Studio 2017中给我错误.E1574.虚幻.但构建成功了
- 即使不包含其标头,如何成功向前声明的类编译?
- C++成功复制动态分配的 obj 而不复制 ctor?
- VS2017 版本 15.8.3 成功编译内联方法,而不返回所需值
- std::vector::push_back的代价成功或无效