О реализации шагов std :: string

Я изучал производительность перемещения std::string. В течение самого долгого времени я рассматривал струнные движения как почти свободные, полагая, что компилятор будет встроить все, и он будет включать только несколько дешевых заданий.

Фактически, моя ментальная модель для перемещения в буквальном смысле

string& operator=(string&& rhs) noexcept
{
    swap(*this, rhs);
    return *this;
}

friend void swap(string& x, string& y) noexcept
{
    // for disposition only
    unsigned char buf[sizeof(string)];
    memcpy(buf, &x, sizeof(string));
    memcpy(&x, &y, sizeof(string));
    memcpy(&y, buf, sizeof(string));
}

Насколько я понимаю, это законная реализация, если memcpy изменен для назначения отдельных полей.

К моему большому удивлению, найти gcc-реализацию перемещения предполагает создание новой строки и, возможно, выброс из-за выделения, несмотря на то, что она не является noexcept.

Это даже соответствует? Не менее важно, не стоит ли думать, что движение почти бесплатное?


Смутно, std::vector<char> сводится к тому, что я ожидаю.

реализация clang сильно отличается, хотя есть подозрительный std::string::reserve

Ответ 1

Я только проанализировал версию GCC. Вот что происходит: код обрабатывает разные типы распределителей. Если у распределителя есть черта _S_propagate_on_move_assign или _S_always_equal, то движение будет почти бесплатным, как вы ожидаете. Это operator= if в move operator=:

if (!__str._M_is_local()
    && (_Alloc_traits::_S_propagate_on_move_assign()
      || _Alloc_traits::_S_always_equal()))
          // cheap move
else assign(__str);

Если условие истинно (_M_is_local() означает небольшую строку, описание здесь), то движение будет дешевым.

Если он неверен, он вызывает нормальное assign (а не движущееся). Это тот случай, когда:

  • строка маленькая, поэтому assign сделает простой memcpy (дешевый)
  • или распределитель не имеет признака, всегда равного или не распространяющего-на-move-assign, поэтому присваивание будет выделять (не дешево)

Что это значит?

Это означает, что если вы используете распределитель по умолчанию (или любой распределитель с указанными ранее признаками), то движение по-прежнему почти бесплатное.

С другой стороны, сгенерированный код излишне огромен и может быть улучшен, я думаю. Он должен иметь отдельный код для обработки обычных распределителей или иметь лучший код assign (проблема в том, что assign не проверяет наличие _M_is_local(), но оно проверяет пропускную способность, поэтому компилятор не может решить, требуется ли распределение или нет, поэтому он без необходимости добавляет кодовую ячейку выделения в исполняемый файл - вы можете проверить точные данные в исходном коде).

Ответ 2

Не совсем ответ, но это новая реализация С++ 11 std::string без счетчика ссылок и с небольшой оптимизацией строки, что приводит к объемистому узлу. В частности, небольшая оптимизация строк приводит к тому, что 4 ветки обрабатывают 4 разных сочетания длин источника и цель назначения перемещения.

Когда добавлена опция -D_GLIBCXX_USE_CXX11_ABI=0 чтобы использовать pre С++ - 11 std::string с опорным счетчиком и небольшую оптимизацию строк, код сборки выглядит намного лучше.


не следует ли думать, что движение почти бесплатное?

В " Ничто лучше, чем копировать или перемещать" Роджером Орром, слайды на стр. 47 говорится:

Сравнение копирования и перемещения

  • Многие люди неправильно думают о том, чтобы двигаться как эффективно "свободные",
  • Разница в производительности между копией и перемещением варьируется в широких пределах
  • Для примитивного типа, такого как int, копирование или перемещение, фактически идентичны
  • Перемещение происходит быстрее, чем копирование, когда требуется передать часть объекта только для передачи всего значения