STL 提供的容器可以有多快?(上)

以下内容为本人的学习笔记,如需要转载,请声明原文链接 微信公众号「ENG八戒」mp.weixin.qq.com/s/WFaYXCAKa...

很多同学经常反馈说,C++ STL 提供的容器真的很慢欸!

标准库的容器性能虽然经常被吐槽,但大多属于这几个方面的问题,比如:

  • 我觉得拷贝的开销很大
  • 查表速度太慢了
  • 自己写的可插入 list 比 std::list 快多啦!

其实,STL 标准模板库提供的容器算得上是效率比较高的了,如果自己在使用中经常碰到性能问题,不妨反思一下是否是自己操作比较粗放,再谈优化的问题。

Bjarne Stroustrup 也说过对标准库提供的容器优化前,应该弄清楚是不是真的存在性能问题。代码效率高不高应该以参照物为准,也就是说和谁比的问题。所以对自己的代码性能不要瞎猜,先测试一下,万一还行呢?比如说,拷贝就一定开销大吗?这个真不一定啊。

arduino 复制代码
std::vector<int> vi;
int i = 2;
vi.push_back(i);

像上面这段代码,把整数推到 vector 里需要拷贝内容,但是这样的拷贝非常简单,对效率没有影响,再优化也没有意义。

那么,哪些场景下,优化容器的使用是必须的呢?我们以上面的几个问题为例,展开一下。

拷贝的巨大开销

假设我们有个图片类 Image,用于表示一张图,创建这张图的对象可以通过导入文件路径来创建。需求是把这些实例化的图片对象放到列表中集中管理,一般我们会直接这么写

arduino 复制代码
std::vector<Image> imgs;
Image img("pic.jpg");
imgs.push_back(img);

对于这样的写法,开销大不大完全依赖于被插入元素 Image 的占用空间。将 img 推入 vector 时这里应用了值传递,需要为 img 在 vector 内预留一块内存空间,然后直接拷贝 img 内容到预留空间。被推入的元素如果比较小,显然效率不会受到影响,但是如果一张图片占用几 MB 的大小,那么这个推入元素内容的过程将会非常低效率。实际上,图片稍微高清一些都可能达到几 MB 的 level。

怎么办?为了解决 std::vector 的 「低效率」,有的人可能会自己实现一个定制的容器,比如 XXX_container。

事实上,我们只需要在使用 std::vector 时稍微变通一下,比如把焦点放在元素类型上,就可以去除拖慢容器执行效率的因素。

指针

在系统中,指针实际就是一个代表地址的数值,拷贝指针和拷贝数值的效率是一样的。

某些场合下,元素对象的拷贝语义还是有必要保留的。所以,不妨将 vector<X> 改为 vector<X*>,这样剩下的开销就主要集中在调用元素类型拷贝构造函数身上,而且还不会影响元素对象本身的拷贝语义。

arduino 复制代码
std::vector<Image*> imgs;
Image img("pic.jpg");
imgs.push_back(&img);

移动感知类型

另一方面,在使用 std::vector 时,元素对象类型比较占空间,如果你使用的是现代版 C++,可以选用带有移动感知的类型作为元素类型。比如该类型实现了对应的移动构造函数和移动赋值操作符,利用移动语义就可以进一步提升 vector 的插入执行效率。

因为拷贝旧内容到新空间,包含状态和堆资源两种,状态的拷贝对效率影响有限,而成员指针指向的堆内存资源通常占据主要空间,可以依赖移动语义的高效率而受益。

关于移动语义的介绍,这里不展开了,可以看看笔者之前写的文章 《现代 C++ 的巨大性能飞跃之:移动语义》,本文末尾有跳转阅读链接。

以 std::vector 为例,针对移动感知类型的优化作用在什么地方呢?

  1. 向 vector 追加元素:使用 emplace_back 方法创建新元素并添加到 vector 末尾时,如果参数列表允许直接构建对象,系统会尝试直接在 vector 预留的内存位置上构造对象,避免额外的移动或拷贝。

使用 push_back(T&& value) 时,如果传入的是右值引用(临时对象或明确标记为将要移动的对象),vector 会调用元素类型的移动构造函数,而不是复制构造函数,将传入对象的内容「移动」到 vector 的存储区域内,这能有效地避免元素类型的深拷贝。

如果你对「深拷贝」的概念不是很清楚,可以查阅一下笔者之前的一篇文章《C++ 代码性能空间之极限拉扯:「COW」 真乃神助攻》,本文末尾有跳转阅读链接。

  1. 容器扩容:vector 的容量包含隐藏的预留空间,当新插入的元素超出剩余容量时,系统会分配一块更大的内存区域,将现有元素移动(而不是复制)到新内存区域,然后再插入新元素。对于移动构造的元素来说,这意味着只需调整内部资源的所有权,而非复制资源本身。

  2. 插入操作:对于像 insert 这样的插入操作,当需要移动现有元素以腾出空间,用于插入移动语义的元素对象时,也会优先使用移动操作而非复制。

  3. 赋值、swap 和其他重新分配等操作:在涉及容器整体交换或容器自身赋值等操作时,vector 同样会利用移动赋值运算符(operator=)来进行更高效的资源转移。

来看一下下面的示例代码:

c 复制代码
class MoveAwareType
{
public:
   MoveAwareType() {
      std::cout << "Default constructor" << std::endl;
   }
   MoveAwareType(const MoveAwareType&) {
      std::cout << "Copy constructor" << std::endl;
   }
    MoveAwareType(MoveAwareType&&) {
      std::cout << "Move constructor" << std::endl;
   }
   MoveAwareType& operator=(const MoveAwareType&) {
      std::cout << "Copy assignment operator" << std::endl;
      return *this;
   }
   MoveAwareType& operator=(MoveAwareType&&) {
      std::cout << "Move assignment operator" << std::endl;
      return *this;
   }
   ~MoveAwareType() {
      std::cout << "Destructor" << std::endl;
   }
};

int main() {
   std::vector<MoveAwareType> vec;
   MoveAwareType obj;
   vec.push_back(obj);
   vec.push_back(MoveAwareType());
   return 0;
}

上面的操作中,vec.push_back(obj) 会调用类 MoveAwareType 拷贝构造函数或者移动构造函数,将 obj 的副本或被移动后的对象添加到 vec 中。具体是调用拷贝构造函数还是移动构造函数,取决于编译器的优化和配置。

vec.push_back(MoveAwareType()); 创建一个临时的 MoveAwareType 对象,并将其添加到 vec 中。由于是临时对象,所以是不具名的,必然会触发调用类 MoveAwareType 的移动构造函数或移动赋值运算符将内容移动到 vec 中。

如果把前面的 Image 类型作为元素类型呢?

这就要求 Image 类型需要实现移动语义了,而且占空间的图像数据应该被抽象到单独的数据类中,方便移动。


全文未结束,本文只是上篇。如果各位同学朋友有什么疑问可以联系笔者,当然笔者也愿意和你进一步探讨这方面的问题。另外,八戒有自己的技术圈朋友群,如果读者朋友想进群交流技术问题,欢迎联系我。下拉到文章底部有我的联系方式!

最后,非常感激各位朋友的点 「赞」 和点击 「在看」,谢谢!


prompt:一个会写代码的牛仔,帽子印有 Cpp 字样,背着袋子,在西部荒野骑马狂奔,数字艺术


相关推荐
风清扬_jd3 分钟前
Chromium 中JavaScript Fetch API接口c++代码实现(二)
javascript·c++·chrome
冷白白21 分钟前
【C++】C++对象初探及友元
c语言·开发语言·c++·算法
睡觉然后上课39 分钟前
c基础面试题
c语言·开发语言·c++·面试
qing_0406031 小时前
C++——继承
开发语言·c++·继承
ya888g1 小时前
GESP C++四级样题卷
java·c++·算法
小叶学C++1 小时前
【C++】类与对象(下)
java·开发语言·c++
NuyoahC1 小时前
算法笔记(十一)——优先级队列(堆)
c++·笔记·算法·优先级队列
FL16238631292 小时前
[C++]使用纯opencv部署yolov11-pose姿态估计onnx模型
c++·opencv·yolo
sukalot2 小时前
windows C++-使用任务和 XML HTTP 请求进行连接(一)
c++·windows
ぃ扶摇ぅ3 小时前
Windows系统编程(三)进程与线程二
c++·windows