包含容器的对象的C++布局
C++ layout of an object containing containers
作为一个有着丰富汇编语言经验和旧习惯的人,我最近用C++做了一个项目,使用了C++03和C++11必须提供的许多功能(主要是容器类,包括Boost中的一些)。它出奇地简单——我尽可能地支持简单性,而不是过早的优化。当我们进入代码审查和性能测试时,我相信有些老手会因为看不到每个字节是如何被操作的而感到不安,所以我想拥有一些先进的弹药。
我定义了一个类,它的实例成员包含几个向量和映射。而不是"指向"矢量和地图的指针。我意识到,我根本不知道我的对象占用了多少连续空间,也不知道频繁清理和重新填充这些容器可能会对性能产生什么影响。
一旦实例化,这样的对象会是什么样子?
形式上,对实现没有任何约束除标准中规定的以外界面和复杂性。实际上,即使不是全部,也是大多数实现源于相同的代码库,并且相像的
矢量的基本实现是三个指针。这个向量中对象的实际内存是动态的已分配。根据矢量的"生长"方式区域可能包含额外的内存;三个指针指向内存的开始,当前最后一个字节之后的字节已使用,以及分配的最后一个字节之后的字节。也许实现的最重要的方面是分离分配和初始化:矢量将在在许多情况下,分配的内存超出了需要,而没有在其中构造对象,并且只构造对象当需要时。此外,当删除对象或清除矢量,它不会释放内存;它只会破坏对象,并将指针更改为已使用的记忆来反映这一点。稍后,当您插入对象时将需要分配。
当添加的对象超出了分配的空间量时,vector将分配一个新的、更大的区域;将对象复制到它,然后销毁旧空间中的对象,并将其删除。由于复杂性的限制,矢量必须扩大面积指数,通过将大小乘以某个固定常数(1.5和2是最常见的因素),而不是将其递增一定的固定量。结果是,如果使用push_back
从空增长矢量,将不会过多的重新分配和拷贝;另一个结果是,如果从空开始增长向量,它最终可能使用几乎两倍的尽可能多的内存。如果您使用CCD_ 2进行预分配。
至于地图,复杂性的限制以及它必须被排序意味着必须使用某种平衡树。在我所知道的所有实现中,这是一个经典的红黑树:每个条目在一个节点中单独分配它包含两个或三个指针,加上一个布尔值,在除了数据之外。
我可以补充一下,以上内容适用于集装箱。当未优化时,将添加额外的指针,将所有迭代器链接到容器,以便在容器一些会使他们无效的事情,这样他们就可以做边界检查。
最后:这些类是模板,所以在实践中访问源,并可以查看它们。(诸如异常安全有时会减少实现直截了当,但实现使用g++或VC++并不是很难理解。)
map
是一个二叉树(有一些种类,我相信它通常是一个红黑树),所以map
本身可能只包含一个指针和一些内务数据(例如元素数量)。
与任何其他二叉树一样,每个节点都将包含两个或三个指针(两个用于"左&右"节点,也许一个指向上面的前一个节点,以避免必须遍历整个树才能找到前一个或多个节点所在的位置)。
一般来说,vector
应该不会明显比常规数组慢,当然也不会比您自己使用指针实现的可变大小数组差。
向量是数组的包装器。向量类包含一个指向连续内存块的指针,并且不知何故知道它的大小。当您清除一个向量时,它通常会保留其旧的缓冲区(取决于实现),以便下次重用它时,分配更少。如果将向量的大小调整为当前缓冲区大小以上,它将不得不分配一个新的缓冲区。重用和清除相同的向量来存储对象是有效的。(std::string
类似)。如果您想知道一个向量在其缓冲区中到底分配了多少,请调用capacity
函数并将其乘以元素类型的大小。您可以调用reserve
函数来手动增加缓冲区大小,以期望向量很快会占用更多元素。
地图比较复杂,所以我不知道。但如果你需要一个关联容器,你也必须在C中使用一些复杂的东西,对吧?
我只是想在其他人的答案中添加一些我认为重要的东西。
首先,默认的(在我看到的实现中)sizeof(std::vector<T>)
是常量,由三个指针组成。以下是GCC 4.7.2 STL标题的摘录,相关部分:
template<typename _Tp, typename _Alloc>
struct _Vector_base
{
...
struct _Vector_impl : public _Tp_alloc_type
{
pointer _M_start;
pointer _M_finish;
pointer _M_end_of_storage;
...
};
...
_Vector_impl _M_impl;
...
};
template<typename _Tp, typename _Alloc = std::allocator<_Tp> >
class vector : protected _Vector_base<_Tp, _Alloc>
{
...
};
这就是三分球的来源。我认为他们的名字不言自明。但是还有一个基类——分配器。这就引出了我的第二点。
其次,std::vector< T, Allocator = std::allocator<T>>
采用第二个模板参数,它是一个处理内存操作的类。它是通过这个类的函数向量来做内存管理的。有一个默认的STL分配器std::allocator<T>>
。它没有数据成员,只有allocate
、destroy
等函数。它的内存处理基于new/delete
。但您可以编写自己的分配器,并将其作为第二个模板参数提供给std::vector
。它必须遵守某些规则,比如它提供的功能等,但内存管理是如何在内部完成的——这取决于你,只要它不违反std::vector
所依赖的逻辑。它可能会引入一些数据成员,这些成员将通过上面的继承添加到sizeof(std::vector)
中。它还为您提供了"对每一位的控制"。
基本上,vector
只是一个指向数组的指针,以及它的容量(分配的总内存)和大小(实际使用的元素):
struct vector {
Item* elements;
size_t capacity;
size_t size;
};
当然,由于封装,所有这些都隐藏得很好,用户永远无法直接处理血腥的细节(重新分配、在需要时调用构造函数/析构函数等)。
至于你关于清除的性能问题,这取决于你如何清除矢量:
- 用临时空向量(通常的习惯用法)交换它将删除旧数组:
std::vector<int>().swap(myVector);
- 使用
std::vector<>::reserve()
0或resize(0)
将擦除所有项目,并保持分配的内存和容量不变
如果您关心效率,IMHO需要考虑的要点是提前调用reserve()
(如果可以的话),以便预分配阵列,避免无用的重新分配和复制(或使用C++11移动)。当向向量中添加大量项目时,这会产生很大的差异(众所周知,动态分配的成本非常高,因此减少它可以大大提高性能)。
关于这件事还有很多要说的,但我相信我已经涵盖了基本的细节。如果你需要更多关于某一点的信息,请毫不犹豫地询问。
关于地图,它们通常是使用红黑树实现的。但该标准并没有强制要求这样做,它只给出了功能和复杂性要求,因此任何其他符合要求的数据结构都是可以使用的。我不得不承认,我不知道RB树是如何实现的,但我想,再一次,映射至少包含一个指针和一个大小。
当然,每种容器类型都是不同的(例如,无序映射通常是哈希表)。
- 如何使用C/C++在MacOSX中获得键盘布局
- Vulkan验证层不断在VkQueuePresentKHR()上抛出图像布局错误
- 布局兼容类型的并集
- Qt自定义QPush按钮未显示在布局上
- 按钮悬停在 QT 中垂直布局的选项卡小部件中不起作用
- 调整布局上的 QGraphicsView 小部件的大小
- 如何在qt中将对象添加到现有布局中?--已解决
- 将布局映射到内存地址
- C++继承的虚拟类的内存布局
- 检查nullptr是否100%保护内存布局不受segfault影响
- C++ Python 的扩展 - 安全内存访问和内存布局
- QTree查看新行,没有布局就不可见已更改
- 类似元组的类模板的反向内存布局
- C++对象布局是否必须静态定义?
- 集合布局上的 Qt 分割错误
- QT将图像从缩略图拖放到网格布局(QVTKOpenGLWidget)?
- 更正GLSL无绑定纹理句柄中的结构布局
- POD类型是否完全等同于琐碎的标准布局类型
- 聚合类型是否意味着它也是标准布局
- 将小部件添加到布局后,QStylesheet 不起作用