C++11中emplace_back与预分配后移动的性能及代码质量对比咨询
在C++11中两种vector初始化实现的性能与代码质量对比
先把两个函数的代码贴出来,方便对照分析:
void f2(const std::vector<std::string>& users) { std::vector<std::pair<std::string, int>> userOccurancies(users.size()); auto userOcIt = userOccurancies.begin(); for (const auto & user : users) { userOcIt->first = std::move(user); userOcIt->second = 0; userOcIt++; } } void f(const std::vector<std::string>& users) { std::vector < std::pair<std::string, std::size_t>> userCount; userCount.reserve(users.size()); for (auto& user : users) { userCount.emplace_back(user, 0); } }
性能分析:为什么f更快?
首先要戳破一个容易被忽略的细节:f2里的std::move(user)根本没起到移动的作用。因为user是const std::string&类型,std::move(user)会生成一个const std::string&&,而std::string的移动构造函数只接受非const的右值引用(std::string&&),所以这里实际上触发的是std::string的拷贝构造,而非移动构造。
接下来对比两者的执行流程:
f2的冗余开销:- 先构造一个大小为
users.size()的vector,每个std::pair都是默认构造的:std::string默认构造为空字符串,int默认初始化为0。 - 循环中对每个
pair的first进行拷贝赋值(移动无效),second重新赋值为0(这一步完全冗余,因为默认已经是0了)。
这里多了两次额外操作:std::string的默认构造,以及后续的赋值操作,比直接构造要多不少开销。
- 先构造一个大小为
f的高效流程:- 先调用
reserve(users.size())预分配足够内存,避免后续扩容的内存拷贝开销。 - 循环中用
emplace_back(user, 0)直接在vector的内存中构造std::pair:用user拷贝构造std::string,同时初始化std::size_t为0。
整个过程是直接构造目标对象,没有额外的默认构造和赋值步骤,步骤更少,开销更低。
- 先调用
为什么性能分析结果随调用顺序变化?
这是典型的缓存预热效应:第一次调用函数时,相关的内存页可能还不在CPU缓存中,需要从内存加载,速度较慢;第二次调用时,数据已经在缓存里了,访问速度会快很多。所以性能测试时,应该先进行几次“预热”调用,然后多次测试取平均值,才能得到更准确的结果。
代码质量分析:f更符合现代C++风格
- 简洁性:
f的代码更短,逻辑更直观,不需要手动维护迭代器,也不需要提前构造元素再修改。 - 可读性:
emplace_back的语义很明确——直接在容器内构造元素,读者一眼就能明白代码的意图;而f2需要手动操作迭代器、赋值成员,代码更啰嗦,容易出错(比如忘记递增迭代器,或者误写成员赋值)。 - 安全性:
f2里的冗余赋值(userOcIt->second = 0)是不必要的,而且试图移动const对象的操作容易误导读者,让别人以为是移动实际是拷贝;f的代码没有这类歧义。
总结
在C11中,f的实现既更快,代码质量也更优。它避免了不必要的对象构造和赋值,代码更简洁易读,符合现代C的最佳实践。
内容的提问来源于stack exchange,提问作者user3360601
相关产品推荐
相关产品推荐

