Ефективне об'єднання рядків у C ++


108

Я чув, як кілька людей висловлюють занепокоєння щодо оператора "+" в std :: string та різних обхідних шляхах, щоб прискорити конкатенацію. Чи є щось із цього дійсно необхідне? Якщо так, то який найкращий спосіб об'єднати рядки в C ++?


13
В основному + НЕ є оператором конденсації (оскільки він генерує новий рядок). Використовуйте + = для конкатенації.
Мартін Йорк

1
Оскільки C ++ 11, є важливий момент: оператор + може змінити один із своїх операндів і повернути його в ході переміщення, якщо цей операнд був переданий посиланням rvalue. libstdc++ робить це, наприклад . Отже, зателефонувавши оператору + із тимчасовими компаніями, він може досягти майже як хорошої продуктивності - можливо, аргумент на користь дефолту, заради читабельності, якщо у вас немає еталонів, що показують, що це вузьке місце. Однак стандартизований варіант append()буде оптимальним і читабельним ...
підкреслюй

Відповіді:


85

Додаткових робіт, мабуть, не варто, якщо тільки вам дійсно не потрібна ефективність. Ви, мабуть, матимете набагато кращу ефективність, просто скориставшись оператором + =.

Тепер після цієї відмови, я відповім на ваше фактичне запитання ...

Ефективність класу рядків 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 нічого не знає про максимальний розмір вашої рядка, а також про те, як часто ви будете поєднувати її. Ви можете мати ці знання і можете робити речі на основі наявності цієї інформації. Це призведе до менших перерозподілів.
  • Ви будете керувати буферами вручну, щоб ви могли бути впевнені, що не будете копіювати всю нитку в нові буфери, коли не хочете, щоб це сталося.
  • Ви можете використовувати стек для своїх буферів замість купи, що набагато ефективніше.
  • Оператор string + створить новий об'єкт string і поверне його звідси за допомогою нового буфера.

Вказівки щодо впровадження:

  • Слідкуйте за довжиною струни.
  • Зберігайте вказівник на кінець рядка і початок, або просто початок, і використовуйте початок + довжину як зміщення, щоб знайти кінець рядка.
  • Переконайтеся, що буфер, в якому ви зберігаєте рядок, достатньо великий, тому вам не потрібно перерозподіляти дані
  • Використовуйте strcpy замість strcat, щоб не потрібно перебирати довжину рядка, щоб знайти кінець рядка.

Структура даних канату:

Якщо вам потрібні дійсно швидкі конкатенації, спробуйте скористатися структурою даних мотузки .


6
Примітка: "STL" відноситься до повністю окремої бібліотеки з відкритим кодом, спочатку HP, частина якої була використана як основа для частин стандартної бібліотеки ISO C ++. "std :: string", однак, ніколи не була частиною STL HP, тому посилатись на "STL і" string "разом повністю - неправильно
James Curran

1
Я б не сказав, що неправильно використовувати STL та рядок разом. Дивіться sgi.com/tech/stl/table_of_contents.html
Брайан Р. Бонді

1
Коли SGI взяв на себе технічне обслуговування STL від HP, він був ретро пристосований, щоб він відповідав стандартній бібліотеці (саме тому я сказав "ніколи не входив до STL HP"). Тим не менш, ініціатором std :: string є Комітет ISO C ++.
Джеймс Курран

2
Бічна примітка: Співробітником SGI, який відповідав за підтримку STL протягом багатьох років, був Метт Остерн, який в той же час очолював бібліотечну підгрупу Комітету стандартизації ISO C ++.
Джеймс Курран

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+=можна використовувати, але вам знадобляться потворні дужки, щоб закріпити пріоритет.


Мені довелося перевірити, чи реально це роблять виконавці 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праворуч.
підкреслюй_

Інший фрагмент накладних даних порівняно з operator+=тим, що operator+все-таки повинен повернути значення, тому він повинен move()залежати від того, до якого операнда додається. Але я думаю, що це досить незначні накладні витрати (копіювання пари покажчиків / розмірів) порівняно з глибоким копіюванням всього рядка, так що це добре!
підкреслюй_

11

Для більшості програм це не має значення. Просто напишіть свій код, блаженно не знаючи, як саме працює + оператор, і візьміть справи лише у свої руки, якщо він стане очевидним вузьким місцем.


7
Звичайно, це не варто в більшості випадків, але це насправді не відповідає на його запитання.
Брайан Р. Бонді

1
так. я згоден, просто кажучи: "профіль, то оптимізуйте", можна поставити коментар до питання :)
Йоханнес Шауб - ліб

6
Технічно він запитав, чи це "Необхідно". Вони ні, і це відповідає на це питання.
Саманта Бранхам

Досить справедливо, але це, безумовно, потрібно для деяких додатків. Тож у цих додатках відповідь зводиться до: «взяти справи у свої руки»
Брайан Р. Бонді,

4
@Pesto У світі програмування є збочене поняття, що продуктивність не має значення, і ми можемо просто ігнорувати всю угоду, оскільки комп'ютери продовжують швидше. Вся справа в тому, що люди не програмують на C ++, і це не те, чому вони розміщують запитання при переповненні стека щодо ефективної конкатенації рядків.
MrFox

7

На відміну від .NET System.Strings, рядки std :: C ++ є змінними, і тому їх можна будувати за допомогою простого конкатенації так само швидко, як і за допомогою інших методів.


2
Особливо, якщо ви використовуєте резерв (), щоб зробити буфер достатньо великим для результату перед початком.
Марк Викуп від

я думаю, що він говорить про оператора + =. це також об'єднує, хоча це вироджений випадок. Джеймс був vc ++ mvp, тому я очікую, що він має деяку підказку c ++: p
Йоганнес Шауб - ліб

1
Я ні на секунду не сумніваюся, що він володіє широкими знаннями на C ++, тільки що виникло непорозуміння щодо цього питання. Питання про ефективність оператора +, який повертає нові рядкові об'єкти щоразу, коли він викликається, і, отже, використовує нові буфери символів.
Брайан Р. Бонді

1
так. але потім він попросив, щоб оператор справи + повільно, який найкращий спосіб - це об'єднання. і ось оператор + = вступає в гру. але я згоден, відповідь Джеймса трохи коротка. звучить так, що ми всі могли використовувати оператор +, і це максимально ефективно: p
Йоханнес Шауб - ліб

@ BrianR.Bondy operator+не повинен повертати нову рядок. Реалізатори можуть повернути один із його операндів, модифікований, якщо цей операнд передається через посилання rvalue. libstdc++ робить це, наприклад . Таким чином, під час виклику operator+з тимчасовими розробниками він може досягти такої ж або майже такої самої хорошої продуктивності - що може бути ще одним аргументом на користь неплатежних дій, якщо у вас немає еталонів, що показують, що він являє собою вузьке місце.
підкреслюй_

5

можливо, замість std :: stringstream?

Але я погоджуюсь з думкою, що вам, мабуть, слід просто підтримувати його зрозумілим та зрозумілим, а потім переглядати, чи є у вас справді проблеми.


2
streamstream повільний, див. groups.google.com/d/topic/comp.lang.c++.moderated/aiFIGb6za0w
ArtemGr

1
@ArtemGr streamstream може бути швидким, дивіться codeproject.com/Articles/647856/…
mloskot

4

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

Така ідея була втілена в реалізації STLport std :: string - що не відповідає стандарту через цей точний злом.


Glib::ustring::compose()з прив'язки glibmm до GLib робить таке: оцінює і reserve()s кінцеву довжину на основі наданого рядка формату та varargs, потім append()s кожен (або його форматована заміна) у циклі. Я думаю, що це досить поширений спосіб роботи.
підкреслюй_

4

std::string operator+виділяє новий рядок і кожен раз копіює два рядки операнда. повторіть багато разів, і це стає дорого, O (n).

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


Я не зовсім впевнений, чому це було знято. Стандартом 50% не вимагається Стандартом, але IIRC, що або 100%, є звичайними заходами зростання на практиці. Все інше у цій відповіді видається беззаперечним.
підкреслити_

Через кілька місяців, я вважаю, що це не все так точно, оскільки це було написано довго після дебютування C ++ 11, і перевантаження, operator+де один або обидва аргументи передаються посиланнями rvalue, може уникнути виділення нової рядки взагалі шляхом об'єднання в існуючий буфер один із операндів (хоч вони, можливо, повинні перерозподілитись, якщо він має недостатню ємність).
підкреслюйте_dr

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!");

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
ОП зацікавлена ​​в тому, як ефективно об'єднатись std::string. Вони не просять альтернативного класу рядків.
підкреслюй

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)), ...);
}
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.