You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

C++17循环内局部STL容器插入list时std::move与copy选型问题

C++17 容器emplace_back使用std::move的性能选型分析

问题背景

在如下C++17场景中存在性能优化选型疑问:代码最后一处emplace_back调用显然适合使用std::move,因为执行完成后局部变量idsLocal不再被使用。但循环内的emplace_back调用是否应该用std::move存在争议:传入std::move(idsLocal)可以避免容器拷贝开销,但有人担心move操作转移内存所有权后,后续迭代中idsLocal会丢失reserve预留的内存空间,触发多次动态重分配反而增加开销。
本次测试的固定入参为:limit值为1024,输入共5120个不同id。
最初的两种实现内存分配行为估算:

第一处emplace_back不使用move的场景:
    -> idsLocal执行reserve产生1次malloc
    -> 5次拷贝idsLocal到idsList产生5次malloc

第一处emplace_back使用move的场景:
    -> idsLocal执行reserve产生1次malloc
    -> 5次move idsLocal到idsList产生0次malloc(待验证)
    -> 首次move后idsLocal动态扩容产生40次malloc(每轮limit对应10次push_back触发扩容,共4轮,待验证)

核心疑问:如果内存分配开销较高,使用move的版本是否反而更慢?全场景使用std::move是否有性能收益?背后原理是什么?

测试原始代码

#include <list>
#include <numeric>
#include <vector>

template < class T >
auto divideIntoIdsList( size_t limit, const T &ids, std::list< T > &idsList )
{
  if ( ids.empty() )
    return;

  T idsLocal;
  idsLocal.reserve( limit );
  size_t iIdCount=0;

  for( typename T::const_iterator itIds = ids.begin();
       itIds != ids.end(); ++itIds )
  {
    idsLocal.push_back( *itIds );

    if( ++iIdCount == limit )
    {
        idsList.emplace_back( idsLocal ); // 是否使用std::move(idsLocal)?
        idsLocal.clear();
        iIdCount=0;
    }
  }

  if( iIdCount > 0 )
      idsList.emplace_back( idsLocal ); // 此处使用std::move收益明确
}

int main()
{
    using ids_t = std::vector< size_t >;
    ids_t ids( 5120 );
    std::iota( std::begin( ids ), std::end( ids ), 0 );
    std::list< ids_t > idslist;
    divideIntoIdsList( 1024, ids, idslist );
    return 0;
}

核心原理

首先明确两个所有主流STL实现(GCC libstdc++、Clang libc++、MSVC STL)都遵循的vector行为:

  • std::vector的移动构造是严格的常数时间操作,仅转移内部堆内存指针、大小、容量三个字段,不会做任何内存分配、元素拷贝操作。移动完成后,源vector处于合法但未指定状态,主流实现中移动后的源vectorsize()和capacity()均为0,原持有的堆内存完全归新构造的vector所有。
  • vector::clear()仅会销毁容器内元素、将size()置为0,不会改变capacity,也不会释放已持有的堆内存——但如果vector本身已经因为move操作丢失了内存所有权(capacity为0),clear()也不可能凭空重新分配内存。

你之前的估算存在两个偏差:

  • 「move后会产生40次malloc」的结论不准确:从空vector开始插入1024个元素,以主流实现2倍扩容因子计算,单轮插入仅会触发11次内存分配(容量从0逐步增长到1、2、4、8、16、32、64、128、256、512、1024),4轮合计44次分配,但每次扩容时旧内存会立刻被释放,且分配的内存块从小到大逐步增长,分配器对这种小内存块的分配效率远高于直接分配1024个元素的大块内存。
  • 就算完全不做额外优化,直接在循环内使用move,总开销依然低于拷贝版本:拷贝版本每次插入都要分配1次1024元素大小的内存,再拷贝1024个元素,合计5次大块内存分配+5120次元素拷贝;而无额外优化的move版本,虽然有44次小内存分配,但总元素拷贝次数仅为4092次(每次扩容时拷贝旧元素的总量),且小内存分配的开销远低于大块内存分配,实际运行速度依然更快。

最优实现方案

完全不需要为了避免扩容而放弃move的收益,只需要在循环内move+clear之后补一行reserve(limit),就可以彻底消除后续扩容的开销,此时总开销为:

  • 初始reserve:1次内存分配
  • 5次move操作:0次内存分配、0次元素拷贝
  • 4次循环内clear后的reserve:4次内存分配

总内存分配次数为5次,比不使用move的版本(6次分配)更少,且全程没有额外的元素拷贝开销,性能达到最优。
对应修改后的循环片段:

if( ++iIdCount == limit )
{
    idsList.emplace_back( std::move(idsLocal) );
    idsLocal.clear();
    idsLocal.reserve(limit); // 补这一行即可彻底避免后续扩容
    iIdCount=0;
}

最后一处emplace_back直接使用std::move(idsLocal)即可,不需要再做reserve,因为函数执行完后idsLocal会立刻被销毁。

最终结论

所有emplace_back调用处都应该使用std::move,只要在循环内move后补一次reserve,就能获得远高于拷贝版本的性能;就算忘记补reserve,move版本的性能也不会低于拷贝版本,不存在「move反而更慢」的情况。

内容的提问来源于stack exchange,提问作者SoulfreezerXP

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.30 10:39:20