为什么 std::set 容器使用的内存比其数据大小多得多?

Why does std::set container use much more memory than the size of its data?

本文关键字:数据 多得多 内存 set std 为什么      更新时间:2023-10-16

例如,我们有 10^7 个 32 位整数。将这些整数存储在数组中的内存使用量为 32*10^7/8=40MB。但是,将 10^7 个 32 位整数插入到集合中需要超过 300MB 的内存。法典:

#include <iostream>
#include <set>
int main(int argc, const char * argv[]) {
std::set<int> aa;
for (int i = 0; i < 10000000; i++)
aa.insert(i);
return 0;
}

其他容器如mapunordered_set在类似的测试中占用更多的内存。我知道集合是用红黑树实现的,但数据结构本身并不能解释高内存使用率。

我想知道这5~8倍原始数据存储器使用背后的原因,以及一些解决方法/替代方案,以实现更高效的内存设置。

让我们来看看 GCC 中的 std::set 实现(在其他编译器中没有太大区别(。 std::set 在 GCC 上实现为一棵红黑树。每个节点都有一个指向父节点、左节点和右节点的指针以及一个颜色枚举器(_S_red 和 _S_black(。这意味着除了 int(可能是 4 个字节(之外,还有 3 个指针(对于 64 位系统,8 * 3 = 24 个字节(和一个枚举器(因为它在 _Rb_tree_node_base 中的指针之前,它被填充到 8 字节边界,因此实际上它需要额外的 8 个字节(。

到目前为止,我已经为集合中的每个整数计算了 24 + 8 + 4 = 36 个字节。但是由于节点必须对齐到 8 个字节,因此必须填充它,使其可以被 8 整除。这意味着每个节点需要 40 个字节(比 int 大 10 倍(。

但这还不是全部。每个这样的节点都由std::allocator分配。此分配器使用new来分配每个节点。由于delete不知道要释放多少内存,因此每个节点也有一些与堆相关的元数据。元数据至少应该包含分配块的大小,通常需要 8 个字节(理论上,在大多数情况下可以使用某种霍夫曼编码并仅存储 1 个字节,但我从未见过有人这样做(。

考虑到所有因素,每个int节点的总数为 48 字节。这是int的 12 倍.集合中的每个int比数组或向量中花费的多 12 倍。

您的数字表明您使用的是 32 位系统,因为您的数据只需要 300 MB。对于 32 位系统,指针需要 4 个字节。这使得节点中的红黑树相关数据为 3 * 4 + 4 = 16 字节 + int 的 4 + 元数据的 4。每个int总共有 24 个字节,而不是 4 个字节。这使得它比大集合的矢量大 6 倍。这些数字表明堆元数据需要 8 个字节,而不仅仅是 4 个字节(可能是由于对齐约束(。

因此,在您的系统上,而不是 40MB(如果std::vector(,预计需要 280MB。

如果你想节省一些花生,你可以为你的套装使用非标准的分配器。您可以通过使用 boost 的隔离存储节点分配器来避免元数据开销。但就记忆而言,这并不是一个很大的胜利。不过,它可以提高您的性能,因为分配器比newdelete中的代码更简单。