Эффективная конкатенация строк в C ++

108

Я слышал, как несколько людей выражали беспокойство по поводу оператора "+" в std :: string и различных обходных путей для ускорения конкатенации. Действительно ли это необходимо? Если да, то как лучше всего объединить строки в C ++?

снег
источник
13
По сути, + НЕ является оператором конкатенации (поскольку он генерирует новую строку). Используйте + = для конкатенации.
Мартин Йорк,
1
Начиная с C ++ 11, есть важный момент: operator + может изменять один из своих операндов и возвращать его по ходу, если этот операнд был передан по ссылке rvalue. libstdc++ делает это, например . Таким образом, при вызове operator + с временными библиотеками он может достичь почти такой же хорошей производительности - возможно, аргумент в пользу его использования по умолчанию для удобства чтения, если нет тестов, показывающих, что это узкое место. Тем не менее, стандартизированная переменная append()была бы оптимальной и удобочитаемой ...
underscore_d

Ответы:

86

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

Теперь, после этого отказа от ответственности, я отвечу на ваш вопрос ...

Эффективность строкового класса STL зависит от реализации используемого вами STL.

Вы можете гарантировать эффективность и больший контроль , выполняя конкатенацию вручную с помощью встроенных функций c.

Почему оператор + неэффективен:

Взгляните на этот интерфейс:

template <class charT, class traits, class Alloc>
basic_string<charT, traits, Alloc>
operator+(const basic_string<charT, traits, Alloc>& s1,
          const basic_string<charT, traits, Alloc>& s2)

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

Почему можно сделать его более эффективным:

  • Вы гарантируете эффективность вместо того, чтобы доверять делегату, который сделает это эффективно за вас
  • класс std :: string ничего не знает ни о максимальном размере вашей строки, ни о том, как часто вы будете с ней соединяться. У вас могут быть эти знания и вы можете делать что-то, основываясь на этой информации. Это приведет к меньшему количеству перераспределений.
  • Вы будете управлять буферами вручную, чтобы быть уверенным, что вы не скопируете всю строку в новые буферы, если вы этого не хотите.
  • Вы можете использовать стек для своих буферов вместо кучи, что намного эффективнее.
  • Оператор строка + создаст новый строковый объект и, следовательно, вернет его, используя новый буфер.

Соображения по реализации:

  • Следите за длиной струны.
  • Держите указатель на конец строки и начало или только на начало и используйте начало + длина в качестве смещения, чтобы найти конец строки.
  • Убедитесь, что буфер, в котором вы храните свою строку, достаточно велик, поэтому вам не нужно перераспределять данные
  • Используйте strcpy вместо strcat, чтобы вам не нужно было перебирать длину строки, чтобы найти конец строки.

Структура данных веревки:

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

Брайан Р. Бонди
источник
6
Примечание. «STL» относится к полностью отдельной библиотеке с открытым исходным кодом, первоначально принадлежащей HP, часть которой использовалась в качестве основы для частей стандартной библиотеки C ++ ISO. «std :: string», однако, никогда не входил в состав HP STL, поэтому совершенно неправильно ссылаться на «STL и« строку »вместе»
Джеймс Карран,
1
Я бы не сказал, что использовать STL и строку вместе - неправильно. См. Sgi.com/tech/stl/table_of_contents.html
Брайан Р. Бонди,
1
Когда SGI взяла на себя обслуживание STL от HP, она была модернизирована, чтобы соответствовать стандартной библиотеке (вот почему я сказал «никогда не входил в STL HP»). Тем не менее, создателем std :: string является комитет ISO C ++.
Джеймс Карран,
2
Примечание: сотрудником SGI, который отвечал за поддержку STL в течение многих лет, был Мэтт Остерн, который в то же время возглавлял библиотечную подгруппу Комитета по стандартизации C ++ ISO.
Джеймс Карран,
4
Не могли бы вы прояснить или дать некоторые моменты, почему вы можете использовать стек для ваших буферов вместо кучи, что намного эффективнее. ? Откуда такая разница в эффективности?
h7r
76

Зарезервируйте последнее место раньше, а затем используйте метод добавления с буфером. Например, предположим, что вы ожидаете, что ваша окончательная длина строки будет составлять 1 миллион символов:

std::string s;
s.reserve(1000000);

while (whatever)
{
  s.append(buf,len);
}
Карлос А. Ибарра
источник
17

Я бы не беспокоился об этом. Если вы делаете это в цикле, строки всегда будут предварительно выделять память, чтобы минимизировать перераспределение - просто используйте operator+=в этом случае. И если вы делаете это вручную, что-то вроде этого или дольше

a + " : " + c

Затем он создает временные объекты, даже если компилятор может удалить некоторые копии возвращаемых значений. Это связано с тем, что в последовательно вызываемом operator+элементе неизвестно, ссылается ли ссылочный параметр на именованный объект или на временный объект, возвращенный из вспомогательного operator+вызова. Я бы предпочел не беспокоиться об этом, пока не профилировал. Но давайте возьмем пример, чтобы показать это. Сначала мы введем круглые скобки, чтобы сделать привязку понятной. Я помещаю аргументы сразу после объявления функции, которое используется для ясности. Ниже я показываю, что получается в результате:

((a + " : ") + c) 
calls string operator+(string const&, char const*)(a, " : ")
  => (tmp1 + c)

Теперь, помимо этого, tmp1это то , что было возвращено первым вызовом оператора + с показанными аргументами. Мы предполагаем, что компилятор действительно умен и оптимизирует копию возвращаемого значения. Таким образом, мы получаем одну новую строку, которая содержит конкатенацию aи " : ". Вот что происходит:

(tmp1 + c)
calls string operator+(string const&, string const&)(tmp1, c)
  => tmp2 == <end result>

Сравните это со следующим:

std::string f = "hello";
(f + c)
calls string operator+(string const&, string const&)(f, c)
  => tmp1 == <end result>

Он использует одну и ту же функцию для временной и именованной строки! Таким образом, компилятор должен скопировать аргумент в новую строку, добавить к ней и вернуть его из тела operator+. Он не может взять воспоминание о временном и добавить к нему. Чем больше выражение, тем больше копий строк нужно сделать.

Следующие Visual Studio и GCC будут поддерживать семантику перемещения C ++ 1x (дополняя семантику копирования). ) и ссылки на rvalue в качестве экспериментального дополнения. Это позволяет выяснить, ссылается ли параметр на временный или нет. Это сделает такие дополнения удивительно быстрыми, так как все вышеперечисленное будет в одном «конвейере добавления» без копий.

Если это окажется узким местом, вы все равно можете сделать

 std::string(a).append(" : ").append(c) ...

В appendвызовы добавьте аргумент *thisи затем возвращает ссылку на себя. Так что там не делается копирование временных файлов. Или, в качестве альтернативы, operator+=можно использовать, но вам понадобятся уродливые круглые скобки для исправления приоритета.

Йоханнес Шауб - litb
источник
Мне пришлось проверить, что разработчики stdlib действительно это делают. : P libstdc++для operator+(string const& lhs, string&& rhs)делает return std::move(rhs.insert(0, lhs)). Тогда, если оба являются временными, его, operator+(string&& lhs, string&& rhs)если lhsимеется достаточная доступная емкость, будет просто напрямую append(). Я думаю, что это рискует быть медленнее, чем operator+=если бы lhsне было достаточно емкости, поскольку тогда он возвращается к rhs.insert(0, lhs), что не только должно расширять буфер и добавлять новое содержимое, например append(), но также должно перемещаться по исходному содержимому rhsсправа.
underscore_d
Другая часть накладных расходов по сравнению с тем operator+=, что operator+все еще должно возвращать значение, поэтому оно должно быть move()любым операндом, к которому оно было добавлено. Тем не менее, я полагаю, что это довольно незначительные накладные расходы (копирование пары указателей / размеров) по сравнению с глубоким копированием всей строки, так что это хорошо!
underscore_d
11

Для большинства приложений это не имеет значения. Просто напишите свой код, блаженно не зная, как именно работает оператор +, и возьмите дело в свои руки, только если он станет очевидным узким местом.

Песто
источник
7
Конечно, в большинстве случаев это того не стоит, но это не совсем ответ на его вопрос.
Брайан Р. Бонди,
1
Да. Я согласен, просто сказав «профиль, затем оптимизируйте», можно поставить комментарий к вопросу :)
Йоханнес Шауб - литб,
6
Технически он спросил, нужны ли они. Это не так, и это дает ответ на этот вопрос.
Саманта Бранхам,
Достаточно честно, но для некоторых приложений это обязательно нужно. Таким образом, в этих приложениях ответ сводится к: «Возьмите дело в свои руки»
Брайан Р. Бонди,
4
@Pesto В мире программирования существует извращенное представление о том, что производительность не имеет значения, и мы можем просто игнорировать всю сделку, потому что компьютеры становятся все быстрее. Дело в том, что люди программируют на C ++ не для этого и не поэтому они задают вопросы об эффективной конкатенации строк при переполнении стека.
MrFox
7

В отличие от .NET System.Strings, строки std :: strings в C ++ являются изменяемыми и поэтому могут быть построены с помощью простой конкатенации так же быстро, как и с помощью других методов.

Джеймс Карран
источник
2
Особенно, если вы используете Reserve (), чтобы перед началом работы сделать буфер достаточно большим для результата.
Марк Рэнсом,
я думаю, он говорит об операторе + =. это также конкатенация, хотя это вырожденный случай. Джеймс был vc ++ mvp, так что я думаю, что он немного разбирается в c ++: p
Johannes Schaub - litb
1
Я ни на секунду не сомневаюсь, что у него обширные познания в C ++, просто возникло недоразумение по поводу вопроса. Был задан вопрос об эффективности оператора +, который при каждом вызове возвращает новые строковые объекты и, следовательно, использует новые буферы символов.
Брайан Р. Бонди,
1
Да. но затем он попросил case operator + медленный, как лучше всего выполнить конкатенацию. и тут в игру вступает оператор + =. но я согласен, что ответ Джеймса немного короче. это звучит так, как будто мы все могли бы использовать operator +, и это очень эффективно: p
Johannes Schaub - litb
@ BrianR.Bondy operator+не должен возвращать новую строку. Разработчики могут вернуть один из его операндов, измененный, если этот операнд был передан по ссылке rvalue. libstdc++ делает это, например . Таким образом, при вызове operator+с временными библиотеками он может достичь такой же или почти такой же хорошей производительности - что может быть еще одним аргументом в пользу его использования по умолчанию, если нет тестов, показывающих, что он представляет собой узкое место.
underscore_d
5

возможно, вместо этого std :: stringstream?

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

Тим
источник
2
stringstream медленный, см. groups.google.com/d/topic/comp.lang.c++.moderated/aiFIGb6za0w
ArtemGr
1
@ArtemGr stringstream может быть быстрым, см codeproject.com/Articles/647856/...
mloskot
4

В Imperfect C ++ Мэтью Уилсон представляет динамический конкатенатор строк, который предварительно вычисляет длину конечной строки, чтобы иметь только одно выделение перед конкатенацией всех частей. Мы также можем реализовать статический конкатенатор, играя с шаблонами выражений .

Такая идея была реализована в реализации STLport std :: string - которая не соответствует стандарту из-за этого точного взлома.

Люк Эрмитт
источник
Glib::ustring::compose()из привязок glibmm к GLib делает следующее: оценивает и reserve()s конечную длину на основе предоставленной строки формата и varargs, затем обрабатывает append()каждый (или его форматированную замену) в цикле. Я думаю, это довольно распространенный способ работы.
underscore_d
4

std::string operator+выделяет новую строку и каждый раз копирует две строки операндов. повторять много раз, и это становится дорого, O (n).

std::string appendи, operator+=с другой стороны, увеличивайте пропускную способность на 50% каждый раз, когда струна должна расти. Что значительно сокращает количество выделений памяти и операций копирования, O (log n).

Тиммеров
источник
Я не совсем понимаю, почему это было отклонено. Цифра 50% не требуется стандартом, но IIRC, или 100%, являются общепринятыми показателями роста на практике. Все остальное в этом ответе не вызывает возражений.
underscore_d
Несколько месяцев спустя, я полагаю, это не так уж и точно, поскольку он был написан спустя много времени после дебюта C ++ 11, и перегрузки, в operator+которых один или оба аргумента передаются по ссылке rvalue, могут полностью избежать выделения новой строки путем объединения в существующий буфер один из операндов (хотя им, возможно, придется перераспределить, если у него недостаточно емкости).
underscore_d
2

Для маленьких струн это не имеет значения. Если у вас большие строки, лучше хранить их в векторном виде или в другой коллекции как части. И адаптируйте свой алгоритм для работы с таким набором данных вместо одной большой строки.

Я предпочитаю std :: ostringstream для сложной конкатенации.

Николай Голубев
источник
2

Как и в большинстве случаев, проще чего-то не делать, чем делать.

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

Если вы хотите вывести данные в файл, используйте потоковую передачу данных, а не создавайте большую строку и выводите ее.

Я никогда не считал необходимым ускорять конкатенацию, если бы удалил ненужную конкатенацию из медленного кода.

Пит Киркхэм
источник
2

Вероятно, лучшая производительность, если вы предварительно выделите (зарезервируете) пространство в результирующей строке.

template<typename... Args>
std::string concat(Args const&... args)
{
    size_t len = 0;
    for (auto s : {args...})  len += strlen(s);

    std::string result;
    result.reserve(len);    // <--- preallocate result
    for (auto s : {args...})  result += s;
    return result;
}

Использование:

std::string merged = concat("This ", "is ", "a ", "test!");
LanDenLabs
источник
0

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

Хитрость заключается в том, чтобы в начале выделить только одно большое выделение.

в

https://github.com/pedro-vicente/table-string

Контрольные точки

Для Visual Studio 2015 отладочная сборка x86, значительное улучшение по сравнению с C ++ std :: string.

| API                   | Seconds           
| ----------------------|----| 
| SDS                   | 19 |  
| std::string           | 11 |  
| std::string (reserve) | 9  |  
| table_str_t           | 1  |  
Педро Висенте
источник
1
OP заинтересован в том, как эффективно объединить std::string. Они не просят альтернативный класс строк.
underscore_d
0

Вы можете попробовать это с резервированием памяти для каждого элемента:

namespace {
template<class C>
constexpr auto size(const C& c) -> decltype(c.size()) {
  return static_cast<std::size_t>(c.size());
}

constexpr std::size_t size(const char* string) {
  std::size_t size = 0;
  while (*(string + size) != '\0') {
    ++size;
  }
  return size;
}

template<class T, std::size_t N>
constexpr std::size_t size(const T (&)[N]) noexcept {
  return N;
}
}

template<typename... Args>
std::string concatStrings(Args&&... args) {
  auto s = (size(args) + ...);
  std::string result;
  result.reserve(s);
  return (result.append(std::forward<Args>(args)), ...);
}
Voltento
источник