为什么libc++的std::string实现占用了libstdc++的3倍内存?

Why does libc++'s implementation of std::string take up 3x memory as libstdc++?

本文关键字:libstdc++ 3倍 内存 实现 libc++ std string 为什么      更新时间:2023-10-16

考虑以下测试程序:

#include <iostream>
#include <string>
#include <vector>
int main()
{
    std::cout << sizeof(std::string("hi")) << " ";
    std::string a[10];
    std::cout << sizeof(a) << " ";
    std::vector<std::string> v(10);
    std::cout << sizeof(v) + sizeof(std::string) * v.capacity() << "n";
}

libstdc++libc++的输出分别为:

8 80 104
24 240 264

如您所见,对于一个简单的程序,libc++占用的内存是其 3 倍。导致这种内存差异的实现有何不同?我是否需要担心以及如何解决它?

这是一个简短的程序,可帮助您探索std::string的两种内存使用:堆栈和堆。

#include <string>
#include <new>
#include <cstdio>
#include <cstdlib>
std::size_t allocated = 0;
void* operator new (size_t sz)
{
    void* p = std::malloc(sz);
    allocated += sz;
    return p;
}
void operator delete(void* p) noexcept
{
    return std::free(p);
}
int
main()
{
    allocated = 0;
    std::string s("hi");
    std::printf("stack space = %zu, heap space = %zu, capacity = %zun",
     sizeof(s), allocated, s.capacity());
}

使用 http://melpon.org/wandbox/可以轻松获取不同编译器/库组合的输出,例如:

GCC 4.9.1:

stack space = 8, heap space = 27, capacity = 2

GCC 5.0.0:

stack space = 32, heap space = 0, capacity = 15

CLANG/libc++:

stack space = 24, heap space = 0, capacity = 22

VS-2015:

stack space = 32, heap space = 0, capacity = 15

(最后一行来自 http://webcompiler.cloudapp.net)

上面的输出还显示了capacity,这是字符串在必须从堆中分配一个新的更大的缓冲区之前可以容纳多少char的度量。 对于 gcc-5.0、libc++ 和 VS-2015 实现,这是短字符串缓冲区的度量。 也就是说,在堆栈上分配的大小缓冲区用于保存短字符串,从而避免了更昂贵的堆分配。

libc++ 实现似乎具有短字符串实现中最小的(堆栈使用率),但包含最大的短字符串缓冲区。 如果计算内存使用量(堆栈 + 堆),libc++ 在所有 4 个实现中,此 2 个字符的字符串的总内存使用量最小。

应该注意的是,所有这些测量都是在64位平台上进行的。 在 32 位上,libc++ 堆栈使用率将下降到 12,小字符串缓冲区将下降到 10。 我不知道 32 位平台上其他实现的行为,但您可以使用上面的代码来找出答案。

你不必担心,标准库实现者知道他们在做什么。

使用 GCC subversion trunk libstdc++ 的最新代码给出以下数字:

32 320 344

这是因为从几周前开始,我将默认std::string实现切换为使用小字符串优化(可容纳 15 个字符的空间),而不是您测试的写入时复制实现。

总结:看起来libstdc++只使用一种char*。事实上,它分配了更多的内存。

因此,您不必担心 Clang 的libc++实现内存效率低下。

来自 libstdc++ 的文档(在详细说明下):

A string looks like this:
                                        [_Rep]
                                        _M_length
   [basic_string<char_type>]            _M_capacity
   _M_dataplus                          _M_refcount
   _M_p ---------------->               unnamed array of char_type

其中_M_p指向字符串中的第一个字符,并将其转换为指向_Rep的指针并减去 1 以获取指向标头的指针。

这种方法具有巨大的优点,即字符串对象只需要一个分配。所有的丑陋都被限制在一对内联函数中,每个函数编译为一个加法指令:_Rep::_M_data() 和字符串::_M_rep();以及分配函数,它获得一个原始字节块,有足够的空间,并在前面构造一个_Rep对象。

您希望_M_data指向字符数组而不是_Rep的原因是,调试器可以看到字符串内容。(可能我们应该添加一个非内联成员来获取调试器使用的_Rep,以便用户可以检查实际的字符串长度。

因此,它看起来像一个char*但在内存使用方面具有误导性。

以前libstdc++基本上使用了这种布局:

  struct _Rep_base
  {
    size_type               _M_length;
    size_type               _M_capacity;
    _Atomic_word            _M_refcount;
  };

这更接近libc++的结果。

libc++使用"短字符串优化"。确切的布局取决于是否定义了_LIBCPP_ABI_ALTERNATE_STRING_LAYOUT。如果已定义,则在字符串较短时,数据指针将按字对齐。有关详细信息,请参阅源代码。

短字符串优化避免了堆分配,因此,如果您只考虑堆栈上分配的部分libstdc++它看起来也比实现成本更高。 sizeof(std::string)仅显示堆栈使用情况,而不显示整体内存使用情况(堆栈 + 堆)。

我没有检查源代码中的实际实现,但我记得在处理C++字符串库时检查过这一点。24 字节字符串实现是典型的。如果字符串的长度小于或等于 16 个字节,则不会从堆中错误定位,而是将字符串复制到大小为 16 字节的内部缓冲区中。否则,它会恶意存储内存地址等。这种次要缓冲实际上有助于提高运行时性能。

对于某些编译器,可以选择关闭内部缓冲区。