Ненужное опустошение перемещенного из std :: string - PullRequest
0 голосов
/ 08 октября 2018

Как в libstdc ++, так и в libc ++ объект перемещенный из std::string становится пустым, даже если исходная сохраненная строка короткая и применяется оптимизация короткой строки.Мне кажется, что это опустошение делает дополнительными и ненужными издержками времени выполнения .Например, вот конструктор перемещения std::basic_string из libstdc ++:

basic_string(basic_string&& __str) noexcept
  : _M_dataplus(_M_local_data(), std::move(__str._M_get_allocator())) {
    if (__str._M_is_local()) 
      traits_type::copy(_M_local_buf, __str._M_local_buf, _S_local_capacity + 1);
    else {
      _M_data(__str._M_data());
      _M_capacity(__str._M_allocated_capacity);
    }
    _M_length(__str.length());
    __str._M_data(__str._M_local_data());  // (1)
    __str._M_set_length(0);                // (2)
  }

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

(2) При очистке строки устанавливается размер строки и сбрасываетсяпервый символ в локальном буфере, который, насколько я знаю, Стандарт не требует .

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

basic_string(basic_string&& __str) noexcept
  : _M_dataplus(_M_local_data(), std::move(__str._M_get_allocator())) {
    if (__str._M_is_local()) {
      traits_type::copy(_M_local_buf, __str._M_local_buf, _S_local_capacity + 1);
      _M_length(__str.length());
    }
    else {
      _M_data(__str._M_data());
      _M_capacity(__str._M_allocated_capacity);
      _M_length(__str.length());
      __str._M_data(__str._M_local_data());  // (1)
      __str._M_set_length(0);                // (2)
    }
  }

Ответы [ 2 ]

0 голосов
/ 18 октября 2018

Я знаю, что думал о том, обнулить ли перемещенную строку при реализации версии libstdc ++, но я не помню причин, по которым я решил обнулить ее.Я думаю, что, вероятно, решил, что оставив пустую перенесенную строку будет следовать принципу наименьшего удивления.Наиболее «очевидное» состояние для строки «от» - быть пустым, даже если непустое значение иногда будет работать немного лучше.

Как предлагается в комментариях, оно позволяет избежать взлома.любой код, который (возможно непреднамеренно) полагался на пустую строку.Я не думаю, что это было одним из моих соображений, хотя.Код C ++ 11, основанный на семантике строк COW, будет нарушен не только пустыми, но не полностью перенесенными строками.

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

  basic_string(basic_string&& __str) noexcept
  : _M_dataplus(_M_local_data(), std::move(__str._M_get_allocator()))
  {
    memcpy(_M_local_buf, __str._M_local_buf, sizeof(_M_local_buf));
    _M_length(__str.length());
    if (!__str._M_is_local())
      {
        _M_data(__str._M_data());
        __str._M_data(__str._M_local_data());
        __str._M_set_length(0);
      }
  }
0 голосов
/ 08 октября 2018

В случае libc ++ конструктор перемещения строк очищает исходный код, но это не является ненужным.Действительно, автором этой строковой реализации был тот же человек, который руководил предложением семантики перемещения для C ++ 11.; -)

Эта реализация строки libc ++ на самом деле была разработана из членов перемещения наружу!

Вот код с некоторыми ненужными деталями (например, режим отладки):

template <class _CharT, class _Traits, class _Allocator>
basic_string<_CharT, _Traits, _Allocator>::basic_string(basic_string&& __str)
        _NOEXCEPT
    : __r_(_VSTD::move(__str.__r_))
{
    __str.__zero();
}

В двух словах, этот код копирует все байты источника, а затем обнуляет все байты источника.Одно замечание: ветвления нет: этот код делает то же самое для длинных и коротких строк.

Режим длинных строк

В «длинном режиме»,макет состоит из 3 слов, указателя данных и двух целочисленных типов для хранения размера и емкости, минус 1 бит для флага long / short.Плюс пробел для распределителя (оптимизированный для пустых распределителей).

Таким образом, это копирует указатель / размеры, а затем обнуляет источник, чтобы освободить владельца указателя.Это также устанавливает источник в «короткий режим», так как короткий / длинный бит означает короткий в нулевом состоянии.Также все нулевые биты в коротком режиме представляют короткую строку нулевого размера, отличную от нуля.

Режим короткой строки

Если источником является короткая строка,код идентичен: байты копируются, а исходные байты обнуляются.В коротком режиме нет самореферентных указателей, поэтому копирование байтов является правильным алгоритмом.

Теперь верно, что в «коротком режиме» обнуление 3-х слов источника может кажется ненужным, но для этого нужно было бы проверить длинный / короткий бит и нулевые байты в длинном режиме.Выполнение этой проверки и перехода на самом деле обходится дороже, чем просто обнуление трех слов из-за случайного неправильного предсказания перехода (разрыв конвейера).

Вот оптимизированная сборка x86 (64-битная) для libc ++string Конструктор перемещения.

std::string
test(std::string& s)
{
    return std::move(s);
}

__Z4testRNSt3__112basic_stringIcNS_11char_traitsIcEENS_9allocatorIcEEEE: ## @_Z4testRNSt3__112basic_stringIcNS_11char_traitsIcEENS_9allocatorIcEEEE
    .cfi_startproc
## %bb.0:
    pushq   %rbp
    .cfi_def_cfa_offset 16
    .cfi_offset %rbp, -16
    movq    %rsp, %rbp
    .cfi_def_cfa_register %rbp
    movq    16(%rsi), %rax
    movq    %rax, 16(%rdi)
    movq    (%rsi), %rax
    movq    8(%rsi), %rcx
    movq    %rcx, 8(%rdi)
    movq    %rax, (%rdi)
    movq    $0, 16(%rsi)
    movq    $0, 8(%rsi)
    movq    $0, (%rsi)
    movq    %rdi, %rax
    popq    %rbp
    retq
    .cfi_endproc

(без ветвей!)

<aside>

Размер внутреннего буфера для короткой строки также оптимизирован дляпереместить участников.Внутренний буфер «объединен» с 3 словами, необходимыми для «длинного режима», так что sizeof(string) не требует больше места, чем в длинном режиме.Несмотря на этот компактный sizeof (наименьший из 3 основных реализаций), libc ++ обладает самым большим внутренним буфером на 64-битных архитектурах: 22 * ​​1039 *.

Маленький sizeof означает более быстрое перемещение членов, поскольку всеэти члены делают копирование и нулевые байты макета объекта.

См. этот ответ Stackoverflow для получения дополнительной информации о размере внутреннего буфера.

</aside>

Сводка

Итак, в итоге, установка источника на пустую строку необходима в «длинном режиме» для передачи владения указателем, а также необходимо в коротком режиме по соображениям производительности, чтобы избежать прерванного конвейера.

Я не комментирую реализацию libstdc ++, так как я не писал этот код, и ваш вопрос в любом случае уже хорошо справляется с этой задачей.: -)

...