Почему C ++ 11 заставил std :: string :: data () добавить нулевой завершающий символ? - PullRequest
27 голосов
/ 02 июля 2019

Ранее это была работа std::string::c_str(), но на C ++ 11 data() также предоставляет ее, почему символ c_str(), заканчивающийся нулем, был добавлен к std::string::data()? Мне кажется, что это пустая трата циклов ЦП, в тех случаях, когда нулевой завершающий символ вообще не имеет значения и используется только data(), компилятору C ++ 03 не нужно заботиться о терминаторе, и не нужно записывать 0 в терминатор каждый раз при изменении размера строки, но компилятор C ++ 11, из-за data() -null-гарантии, должен тратить циклы на запись 0 каждый раз при изменении размера строки, поэтому так как это потенциально делает код медленнее, я думаю, у них была причина добавить эту гарантию, что это было?

Ответы [ 3 ]

28 голосов
/ 02 июля 2019

Здесь нужно обсудить два момента:

Место для нулевого терминатора

Теоретически реализация C ++ 03 могла бы избежать выделения места длятерминатор и / или, возможно, потребовалось выполнить копии (например, unsharing ).

Тем не менее, все вменяемые реализации выделяли место для нулевого терминатора для поддержки c_str() для начала,потому что в противном случае это было бы практически непригодно, если бы это не было тривиальным вызовом.

Сам нуль-терминатор

Это правда, что некоторые очень (1999), очень старые реализации (2001) записывали \0 каждый c_str() вызов.

Однако основные реализации изменились (2004) или были уже такими (2010), чтобы избежать подобных вещей до выхода C ++ 11, поэтому, когда появился новый стандарт, для многих пользователей ничего не изменилось.

Теперь, должна ли была быть реализована реализация C ++ 03это или нет:

Мне кажется, чтотрата процессорных циклов

Не совсем.Если вы звоните c_str() более одного раза, вы уже напрасно тратите время, записывая его несколько раз.Мало того, вы возитесь с иерархией кеша, что важно учитывать в многопоточных системах.Напомним, что многоядерные / SMT-процессоры начали появляться между 2001 и 2006 , что объясняет переход к современным реализациям, не относящимся к CoW (даже если было несколько многопроцессорных систем на паруза десятилетия до этого).

Единственная ситуация, когда вы могли бы спасти что-либо, это если бы вы никогда не назвали c_str().Однако обратите внимание, что когда вы изменяете размер строки, вы все равно переписываете все.Дополнительный байт будет трудно измеримым.

Другими словами, не при записи терминатора при изменении размера вы подвергаете себя худшей производительности / задержке.Если вы записываете один раз , в то же время вам приходится выполнять копирование строки, поведение производительности становится более предсказуемым, и вы избегаете ловушек производительности, если в конечном итоге вы используете c_str(), особенно в многопоточных системах.

24 голосов
/ 02 июля 2019

Преимущества изменения:

  1. Когда data также гарантирует нулевой терминатор, программисту не нужно знать неясные детали различий между c_str и dataи, следовательно, позволит избежать неопределенного поведения при передаче строк без гарантии нулевого завершения в функции, которые требуют нулевого завершения.Такие функции вездесущи в интерфейсах C, и интерфейсы C часто используются в C ++.

  2. Оператор индекса также был изменен, чтобы разрешить доступ на чтение к str[str.size()].Не разрешать доступ к str.data() + str.size() было бы непоследовательным.

  3. Хотя не инициализация нулевого терминатора при изменении размера и т. Д. Может ускорить эту операцию, она вызывает инициализацию в c_str, что делаетфункция медленнее¹.Случай оптимизации, который был удален, не всегда был лучшим выбором.Учитывая изменение, упомянутое в пункте 2., эта медлительность затронула бы и оператор индекса, что, безусловно, было бы неприемлемо для производительности.Таким образом, нулевой терминатор в любом случае должен был быть там, и поэтому не было бы обратной стороны в том, чтобы гарантировать, что это так.

Любопытная деталь: str.at(str.size()) все еще вызывает исключение.

PS Произошло еще одно изменение, заключающееся в том, что строки хранятся непрерывно (именно поэтому data предоставляется в первую очередь).До C ++ 11 реализации могли использовать веревочные строки и перераспределять при вызове c_str.Ни одна крупная реализация не решила использовать эту свободу (насколько мне известно).

PPS Старые версии libstdc ++ в GCC, например, очевидно, устанавливали нулевой терминатор только в c_str до версии 3.4.Подробнее см. связанный коммит .


factor Фактором этого является параллелизм, который был введен в стандарт языка в C ++ 11.Одновременная неатомарная модификация - неопределенное поведение гонки данных, поэтому компиляторам C ++ разрешено агрессивно оптимизировать и хранить данные в регистрах.Таким образом, реализация библиотеки, написанная на обычном C ++, будет иметь UB для одновременных вызовов .c_str()

На практике (см. Комментарии) наличие нескольких потоков, пишущих одну и ту же , не вызовет проблемы с корректностьюпотому что ASM для реальных процессоров не имеет UB.А правила C ++ UB означают, что несколько потоков на самом деле изменяют a std::string объект (кроме вызова c_str()) без синхронизации - это то, что компилятор + библиотека может предположить, что не произойдет.

Но это могло бы испортить кеш и помешать другим потокам читать его, так что это по-прежнему плохой выбор, особенно для строк, которые могут иметь одновременные читатели.Кроме того, это остановит .c_str() в основном от оптимизации из-за побочного эффекта магазина.

13 голосов
/ 02 июля 2019

Суть вопроса проблематична.

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

что вас огорчает, это одна паршивая mov инструкция по сборке? поверьте мне, это не повлияет на вашу производительность даже на 0,5%.

При написании среды программирования, вы не можете быть одержимы каждой маленькой инструкцией по сборке. Вы должны выбирать свои сражения оптимизации мудро, и оптимизация незаметного нулевого завершения не является одним из них.

В этом конкретном случае совместимость с C намного важнее, чем нулевое завершение.

...