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

22

考虑以下测试程序:

#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倍的内存。导致这种内存差异的实现方式有何不同?我需要担心吗?如何解决?


1
这很可能是因为libstdc++的std::string实现不符合C++11标准,而是使用了写时复制的实现方式,从而带来了尺寸上的节省。请重新运行您的libstdc++测试,使用vstring进行比较并查看结果。 - Praetorian
16
要明确的是,sizeof(std::string)并不代表字符串占用的所有内存,而仅代表字符串类所占用的内存(例如,在堆栈上分配的字符串),并且不包括它所指向的任何数据结构。 - EyasSH
2
这是指数组的测量吗?(因为由于上述相同的原因,这也不准确)。 - EyasSH
1
如果您在使用libstdc++时使用__gnu_cxx::__vstring,则结果会有很大的不同。而EyasSH所说的是,std::string对象的大小不受其管理的字符串长度的影响。 - Praetorian
@Praetorian 这是自2015年以来的事情了;呼! :) - underscore_d
显示剩余2条评论
4个回答

66

这是一个简短的程序,帮助您探索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 = %zu\n",
     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

上面的输出还显示了capacity,它是一个度量标准,表示字符串在必须从堆中分配新的、更大的缓冲区之前可以容纳多少个char。对于gcc-5.0、libc++和VS-2015实现,这是短字符串缓冲区的度量标准。也就是说,在栈上分配大小缓冲区来保存短字符串,从而避免更昂贵的堆分配。
看起来libc++实现具有最小的(栈使用)短字符串实现之一,但却包含最大的短字符串缓冲区之一。如果你计算内存使用量(栈+堆),libc++在这4种实现中对于这个2字符字符串具有最小的总内存使用量。
值得注意的是,所有这些测量都是在64位平台上进行的。在32位上,libc++的栈使用将降至12,小字符串缓冲区将降至10。我不知道其他实现在32位平台上的行为,但你可以使用上述代码来找出答案。(最后一行引用自http://webcompiler.cloudapp.net

4
我很惊讶没有其他实现像libc++一样使用类似的技巧:确保“long”和“short”的容量是奇数/偶数,并将不包含此关键位的所有字节用作小字符串的缓冲区(除了保存长度的一个字节,它显然小于256)。这确实有成本,几乎所有操作都需要检查字符串是短还是长,然后再执行任何操作(尽管如果您连续进行两个操作,可能只需测试一次)。也许这可以弥补浪费空间的代价?可能取决于情况... - Marc Glisse
在Ubuntu 14.04上,我从g++ 4.9.4、g++ 5.4.1和clang++ 3.6.0中获得了相同的结果,都与您在gcc 4.9.1中的结果相符。 - Ruslan
1
默认情况下,它们使用相同的系统安装的标准库。您必须提供一些标志来使用不同的标准库,例如对于Clang使用libc++,可以使用-stdlib=libc++ - Ilya Popov

10

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

使用来自GCC子版本主干libstdc++的最新代码会得到这些数字:

32 320 344

这是因为几周前我更改了默认的std::string实现,使用了小字符串优化(有15个字符的空间)而不是您测试时使用的写时复制实现。


1
确保它符合要求并没有错,但遗憾的是,在类型抹消包装器(如 any)中,它突然变得明显次优,因为它的实现比 sizeof(string) 具有稍小的内部容量来执行小对象优化…… 甚至在使用 pmr::string 时更糟。 - FrankHB

7
总结:看起来像是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以获取头指针。

这种方法的巨大优势在于,一个字符串对象只需要一次分配。所有的丑陋都限制在单个内联函数对中,每个函数编译成一个单独的add指令:_Rep::_M_data()和string::_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)只显示堆栈使用情况而不是总体内存使用情况(堆栈+堆)。


1
libc++总是使用SSO,_LIBCPP_ALTERNATE_STRING_LAYOUT编译时宏只是改变其布局。 - Petr
@Petr 谢谢你,我更新了它。自回答发布以来,宏的名称也发生了改变。现在它叫作 _LIBCPP_ABI_ALTERNATE_STRING_LAYOUT。 - Philipp Claßen

2
我没有检查过源代码中的实际实现,但我记得在开发我的C++字符串库时曾经进行过此类检查。一个24字节的字符串实现是典型的。如果字符串的长度小于或等于16个字节,则不会从堆中分配内存,而是将其复制到大小为16字节的内部缓冲区中。否则,它会malloc并存储内存地址等信息。这种小型缓冲实际上有助于提高运行时间性能。

对于某些编译器,有一个选项可以关闭内部缓冲区。


6
在C++11及其以上版本中,每次使用共享资源都必须是线程安全的。堆是一个共享资源,所以对堆分配/释放例程的调用必须进行同步。这是人们想要短字符串优化的原因之一。 - Marshall Clow
@MarshallClow 有道理,谢谢! - mostruash

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接