包含容器的对象的C ++布局

时间:2013-08-24 22:46:18

标签: c++ object boost containers

作为一个拥有大量汇编语言经验和旧习惯的人,我最近使用c ++ 03和c ++ 11提供的许多功能(主要是容器类,包括一些来自Boost)。这非常简单 - 我尽我所能地尝试简化过早优化。当我们进入代码审查和性能测试时,我确信有些老手会因为没有准确看到每个字节的操作方式而产生动力,所以我想要预先获得一些弹药。

我定义了一个类,其实例成员包含几个向量和映射。不是“指向”矢量和地图的指针。我意识到我没有想到我的对象占用了多少连续空间,或者经常清理和重新填充这些容器会对性能产生什么影响。

这样的对象在实例化后会是什么样的?

5 个答案:

答案 0 :(得分:4)

正式来说,实施没有任何限制 除了标准中规定的那些之外,关于 界面和复杂性。实际上,大多数,如果不是全部的话 实现派生自相同的代码库,并且是公平的 类似。

vector的基本实现是三个指针。该 向量中对象的实际内存是动态的 分配。根据矢量“增长”的方式,动态 区域可能包含额外的内存;三个指针指向 内存的开始,当前最后一个字节之后的字节 used,以及分配的最后一个字节后的字节。也许是 实施的最重要方面是它 分离分配和初始化:向量将在, 在许多情况下,分配比所需更多的内存,没有 在其中构造对象,并且只构造对象 需要的时候。此外,当您删除对象,或清除 向量,它不会释放内存;它只会破坏 对象,并将指针更改为已使用的结尾 记忆来反映这一点。稍后,当您插入对象时,没有 将需要分配。

当您添加超出分配空间量的对象时, 矢量将分配一个新的,更大的区域;将对象复制到 它,然后破坏旧空间中的对象,并删除它。 由于复杂性约束,矢量必须增长区域 通过将大小乘以某个固定常数来指数地增加 (1.5和2是最常见的因素),而不是 将它递增一些固定的数量。结果就是如果你 使用push_back从空中增长向量,不会有 太多的重新分配和副本;另一个结果就是如果你 从空中增长矢量,它最终可以使用几乎两倍 必要的内存。如果你这些问题可以避免 使用std::vector<>::reserve()进行预分配。

至于地图,复杂性约束及其必须的事实 被命令意味着必须使用某种平衡树。 在我所知的所有实现中,这都是经典的 红黑树:每个条目在节点中单独分配 它包含两个或三个指针,加上可能是一个布尔值 除了数据。

我可能会补充说,以上内容适用于优化版本 容器。通常的实现,如果没有优化, 将添加其他指针以将所有迭代器链接到 容器,以便容器可以在容器上标记 会使他们失效的东西,以便他们可以做到 边界检查。

最后:这些类是模板,所以在实践中,你有 访问源,并可以查看它们。 (问题如 异常安全有时会使实现更少 比我们想要的直截了当,但实施 使用g ++或VC ++并不是很难理解。)

答案 1 :(得分:3)

map是二叉树(有些种类,我相信它通常是红黑树),所以map本身可能只包含一个指针和一些内务数据(例如元素数量)。

与任何其他二叉树一样,每个节点将包含两个或三个指针(两个用于“左和右”节点,也许一个到上一个节点,以避免必须遍历整个树以找到以前的节点是)。

通常,vector不应该明显慢于常规数组,并且肯定不比使用指针的自己实现的可变大小数组差。

答案 2 :(得分:1)

vector是数组的包装器。向量类包含指向连续内存块的指针,并以某种方式知道其大小。清除向量时,它通常会保留其旧缓冲区(依赖于实现),以便下次重用它时,分配的次数会减少。如果将矢量调整到当前缓冲区大小以上,则必须分配一个新的。重用和清除相同的向量来存储对象是有效的。 (std::string类似)。如果要确切了解向量在其缓冲区中分配了多少,请调用capacity函数并将其乘以元素类型的大小。您可以调用reserve函数手动增加缓冲区大小,期望向量很快占用更多元素。

地图更复杂,所以我不知道。但是如果你需要一个关联容器,你也必须在C中使用复杂的东西,对吗?

答案 3 :(得分:1)

只是想在其他人的答案中添加一些我认为重要的事情。

首先,默认(在我看到的实现中)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>>。它没有数据成员,只有allocatedestroy等函数。它的内存处理基于new/delete。但是您可以编写自己的分配器并将其作为第二个模板参数提供给std::vector。它必须符合某些规则,例如它提供的功能等,但是如何在内部完成内存管理 - 这取决于你,只要它不违反std::vector依赖的逻辑。它可能会引入一些数据成员,这些成员将通过上面的继承添加到sizeof(std::vector)。它还为您提供“控制每个位”。

答案 4 :(得分:0)

基本上,vector只是一个指向数组的指针,以及它的容量(总分配内存)和大小(实际使用的元素):

struct vector {
    Item* elements;
    size_t capacity;
    size_t size;
};

当然,由于封装,所有这些都被隐藏起来,用户永远不会直接处理血腥细节(重新分配,在需要时调用构造函数/析构函数等)。

关于清算的表现问题,取决于你清除矢量的方式:

  • 使用临时空向量(通常的成语)交换它将删除旧数组:std::vector<int>().swap(myVector);
  • 使用clear()resize(0)将清除所有项目并保持分配的内存和容量不变。

如果你关注效率,恕我直言,要考虑的要点是提前调用reserve()(如果可以的话),以便预先分配数组并避免无用的重新分配和复制(或用C移动) ++ 11)。在向量中添加大量项目时,这会产生很大的不同(众所周知,动态分配非常昂贵,因此减少它可以大大提高性能)。

关于这一点还有很多话要说,但我相信我已经涵盖了必要的细节。不要犹豫,询问您是否需要有关特定点的更多信息。


关于地图,它们通常使用红黑树实现。但是标准并没有强制要求,它只提供功能和复杂性要求,因此适合该法案的任何其他数据结构都是好的。我不得不承认,我不知道如何实现RB树,但我想再说一次,地图至少包含一个指针和一个大小。

当然,每个容器类型都不同(例如,无序映射通常是哈希表)。