Почему реализация libС++ из std::string занимает 3-кратную память как libstdС++?

Рассмотрим следующую тестовую программу:

#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

Вот краткая программа, которая поможет вам изучить использование двух типов памяти 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, например:

gcc 4.9.1:

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

gcc 5.0.0:

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

лязг/LibС++:

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, libС++ и VS-2015 это мера буфера коротких строк. То есть буфер размера, выделенный в стеке, содержит короткие строки, тем самым избегая более дорогого распределения кучи.

Похоже, что реализация libС++ имеет наименьшее (использование стека) реализации коротких строк и все же содержит самый большой из коротких строковых буферов. И если подсчитать общее использование памяти (стек + куча), libС++ имеет наименьшее общее использование памяти для этой 2-символьной строки среди всех 4 этих реализаций.

Следует отметить, что все эти измерения были выполнены на 64-битных платформах. На 32-разрядном уровне использование стека libС++ будет уменьшаться до 12, а буфер с небольшой строкой - до 10. Я не знаю поведения других реализаций на 32-битных платформах, но вы можете использовать приведенный выше код, чтобы узнать.

Ответ 2

Вы не должны беспокоиться, разработчики стандартных библиотек знают, что они делают.

Используя последний код из соединительной линии GCC subversion libstdС++, вы получите следующие цифры:

32 320 344

Это связано с тем, что с нескольких недель назад я включил реализацию по умолчанию std::string для использования оптимизации небольших строк (с пространством для 15 символов) вместо реализации копирования на запись, с которой вы протестировали.

Ответ 3

Сводка: он выглядит только как libstdc++ использует один char*. Фактически, он выделяет больше памяти.

Итак, вы не должны беспокоиться о том, что реализация Clang libc++ неэффективна в памяти.

Из документации libstdС++ (под Подробное описание):

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_ALTERNATE_STRING_LAYOUT).

Оптимизация коротких строк позволяет избежать распределения кучи, поэтому она также выглядит более дорогостоящей, чем реализация libstdc++, если вы рассматриваете только части, выделенные в стеке. sizeof(std::string) показывает только использование стека, а не общее использование памяти (стек + куча).

Ответ 4

Я не проверял фактические реализации в исходном коде, но я помню, как это проверял, когда я работал над своей строковой библиотекой С++. Обычная реализация строки в 24 байта. Если длина строки меньше или равна 16 байтам, вместо malloc'ing из кучи она копирует строку во внутренний буфер размером 16 байт. В противном случае он сохраняет и сохраняет адрес памяти и т.д. Эта небольшая буферизация фактически помогает с точки зрения производительности рабочего времени.

Для некоторых компиляторов есть возможность отключить внутренний буфер.