考虑以下测试程序:
#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 = %zu\n",
sizeof(s), allocated, s.capacity());
}
使用 http://melpon.org/wandbox/ 很容易获得不同编译器/lib 组合的输出,例如:
海湾合作委员会4.9.1:
stack space = 8, heap space = 27, capacity = 2
海湾合作委员会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++ 实现似乎具有最小的(堆栈使用)短字符串实现,但包含最大的短字符串缓冲区。如果计算总内存使用量(堆栈 + 堆),在所有 4 个实现中,libc++ 对于这个 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() 和 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)
仅显示堆栈使用情况,而不显示整体内存使用情况(堆栈+堆)。
我还没有检查源代码中的实际实现,但我记得在我处理 C++ 字符串库时检查过这一点。 24 字节字符串实现是典型的。如果字符串的长度小于或等于 16 字节,则不会从堆进行 malloc,而是将字符串复制到大小为 16 字节的内部缓冲区中。否则,它会分配并存储内存地址等。这种较小的缓冲实际上有助于提高运行时性能。
对于某些编译器,可以选择关闭内部缓冲区。