Наскільки саме std :: string_view швидше, ніж const std :: string &?


221

std::string_viewзробив це на C ++ 17, і його широко рекомендується використовувати замість const std::string&.

Однією з причин є продуктивність.

Чи може хтось пояснити, наскільки саме std::string_view / буде швидше, ніж const std::string&при використанні як тип параметра? (припустимо, що копії в назві не зроблено)


7
std::string_view- це лише абстракція пари (char * початок, char * end). Ви використовуєте його під час створення std::stringкопії, це буде непотрібною копією.
QuestionC

На мою думку, питання полягає не в тому, який з них швидше, а коли їх використовувати. Якщо мені потрібна певна маніпуляція на рядку, і вона не є постійною та / або зберігає початкове значення, string_view є ідеальним, оскільки мені не потрібно робити копію рядка до нього. Але якщо мені потрібно лише перевірити щось на рядку, використовуючи, наприклад, string :: find, тоді посилання краще.
TheArquitect

@QuestionC ви використовуєте його , якщо ви не хочете , щоб ваш API обмежитися std::string(string_view може приймати сирі масиви, вектори, std::basic_string<>з нестандартними параметрами розподільників і т.д. і т.п. і т.д. Та й інші string_views очевидно)
sehe

Відповіді:


213

std::string_view швидше в кількох випадках.

По-перше, std::string const&вимагає, щоб дані знаходилися в a std::string, а не в необробленому масиві C, char const*повертається API API, std::vector<char>виробляється деяким механізмом десеріалізації тощо. Перетворення формату, що ухиляється, дозволяє уникнути копіювання байтів, і (якщо рядок довший ніж SBO¹ для конкретної std::stringреалізації) уникає виділення пам'яті.

void foo( std::string_view bob ) {
  std::cout << bob << "\n";
}
int main(int argc, char const*const* argv) {
  foo( "This is a string long enough to avoid the std::string SBO" );
  if (argc > 1)
    foo( argv[1] );
}

Ніяких розподілів у string_viewсправі не робиться , але було б, якби fooвзяли std::string const&замість а string_view.

Друга справді велика причина - це те, що вона дозволяє працювати з підрядками без копії. Припустимо, ви розбираєте 2-гігабайтний json рядок (!) ². Якщо проаналізувати його std::string, кожен такий вузол розбору, де вони зберігають ім'я або значення вузла, копіює вихідні дані з рядка 2 ГБ в локальний вузол.

Натомість, якщо розібрати їх до std::string_views, вузли посилаються на вихідні дані. Це може заощадити мільйони асигнувань та зменшити вдвічі менше пам'яті під час розбору.

Швидкість, яку ви можете отримати, просто смішна.

Це надзвичайний випадок, але інші випадки "отримати підрядку та працювати з ним" також можуть генерувати гідні прискорення string_view.

Важливою частиною рішення є те, що ви втрачаєте, використовуючи std::string_view. Це не багато, але це щось.

Ви втрачаєте неявне припинення нуля, і це стосується цього. Отже, якщо однаковий рядок буде передано 3 функціям, всі з яких потребують нульового термінатора, перетворення на std::stringодин раз може бути розумним. Таким чином, якщо, як відомо, ваш код потребує нульового термінатора, і ви не очікуєте, що рядки, подані з буферів, створених у стилі C, або подібного, можливо, візьміть a std::string const&. В іншому випадку візьміть std::string_view.

Якщо у вас std::string_viewбув прапор, який зазначав, що він був скасований на нуль (або щось більш фантазійне), він би видалив навіть цю останню причину для використання std::string const&.

Відомий випадок, коли прийом « std::stringне» const&є оптимальним за «a» std::string_view. Якщо після виклику вам потрібно володіти копією рядка на невизначений термін, отримання побічної вартості є ефективним. Ви або будете знаходитись у корпусі SBO (і жодних виділень, лише декілька копій символів для його дублювання), або ви зможете перемістити виділений купою буфер в локальний std::string. Маючи дві перевантаження std::string&&і std::string_viewможе бути швидше, але лише незначно, і це спричинить скромний розрив коду (що може коштувати вам усіх швидкостей).


¹ Невелика оптимізація буфера

² Фактичне використання.


8
Ви також втрачаєте право власності. Що цікавить лише те, що рядок буде повернуто, і, можливо , це буде що-небудь, крім підрядка буфера, який гарантовано витримає досить довго. Власне, втрата власності - це дуже дворука зброя.
Дедупликатор

СБО звучить дивно. Я завжди чув SSO (невелика оптимізація струн)
phuclv

@phu Звичайно; але рядки - не єдине, на чому ви використовуєте трюк.
Якк - Адам Невраумон

@phuclv SSO - це лише специфічний випадок SBO, який означає невелику оптимізацію буфера . Альтернативними термінами є вибір малих даних. , вибір малого об'єкта або вибір невеликого розміру .
Даніель Лангр

59

Один із способів того, що string_view покращує продуктивність, це те, що він дозволяє легко видаляти префікси та суфікси. Під кришкою string_view може просто додати розмір префікса до вказівника до якогось буферного рядка або відняти розмір суфікса з лічильника байтів, це зазвичай швидко. std :: string з іншого боку має копіювати свої байти, коли ви робите щось на кшталт substr (таким чином ви отримуєте новий рядок, який володіє його буфером, але в багатьох випадках ви просто хочете отримати частину оригінальної рядки без копіювання). Приклад:

std::string str{"foobar"};
auto bar = str.substr(3);
assert(bar == "bar");

З std :: string_view:

std::string str{"foobar"};
std::string_view bar{str.c_str(), str.size()};
bar.remove_prefix(3);
assert(bar == "bar");

Оновлення:

Я написав дуже простий орієнтир, щоб додати кілька реальних цифр. Я використав дивовижну бібліотеку бенчмарків google . Орієнтовними функціями є:

string remove_prefix(const string &str) {
  return str.substr(3);
}
string_view remove_prefix(string_view str) {
  str.remove_prefix(3);
  return str;
}
static void BM_remove_prefix_string(benchmark::State& state) {                
  std::string example{"asfaghdfgsghasfasg3423rfgasdg"};
  while (state.KeepRunning()) {
    auto res = remove_prefix(example);
    // auto res = remove_prefix(string_view(example)); for string_view
    if (res != "aghdfgsghasfasg3423rfgasdg") {
      throw std::runtime_error("bad op");
    }
  }
}
// BM_remove_prefix_string_view is similar, I skipped it to keep the post short

Результати

(x86_64 linux, gcc 6.2, " -O3 -DNDEBUG"):

Benchmark                             Time           CPU Iterations
-------------------------------------------------------------------
BM_remove_prefix_string              90 ns         90 ns    7740626
BM_remove_prefix_string_view          6 ns          6 ns  120468514

2
Чудово, що ви надали фактичний орієнтир. Це дійсно показує, що можна отримати у відповідних випадках використання.
Даніель Каміль Козар

1
@DanielKamilKozar Дякую за відгук. Я також вважаю, що орієнтири є цінними, іноді вони все змінюють.
Павло Давидов

47

Є дві основні причини:

  • string_view є фрагментом у існуючому буфері, він не потребує виділення пам'яті
  • string_view передається за значенням, а не за посиланням

Переваги наявності шматочка є декількома:

  • ви можете використовувати його з char const*або char[]без виділення нового буфера
  • ви можете взяти кілька фрагментів і підпунктів у існуючий буфер, не виділяючи їх
  • підрядка - O (1), а не O (N)
  • ...

Покращення та стабільність роботи у всьому світі.


Передача за значенням також має переваги перед проходженням посилань, тому що їх псевдонім.

Зокрема, коли у вас є std::string const&параметр, немає гарантії, що посилання рядка не буде змінено. В результаті компілятор повинен повторно вибирати вміст рядка після кожного виклику в непрозорий метод (вказівник на дані, довжину, ...).

З іншого боку, при передачі string_viewзначення за значенням компілятор може статично визначити, що жоден інший код не може змінювати покажчики довжини та даних, які зараз знаходяться на стеку (або в регістрах). Як результат, він може «кешувати» їх у всіх викликах функцій.


36

Одне, що можна зробити, - це уникати побудови std::stringоб'єкта у випадку неявного перетворення з нульового завершеного рядка:

void foo(const std::string& s);

...

foo("hello, world!"); // std::string object created, possible dynamic allocation.
char msg[] = "good morning!";
foo(msg); // std::string object created, possible dynamic allocation.

12
Можливо, варто сказати, що, const std::string str{"goodbye!"}; foo(str);мабуть , не буде швидше з string_view, ніж із рядком &
Мартін Боннер підтримує Моніку

1
Не string_viewбудеш повільним, оскільки він повинен скопіювати два вказівники на відміну від одного вказівника const string&?
балки

9

std::string_viewв основному просто обгортка навколо const char*. А проходження const char*означає, що в системі буде один менший покажчик порівняно з проходженням const string*(або const string&), тому що string*передбачає щось на кшталт:

string* -> char* -> char[]
           |   string    |

Зрозуміло, що для передачі аргументів const перший вказівник є зайвим.

ps Одним з істотних відмінностей між std::string_viewі const char*, тим не менш, є те, що строки перегляду строків не повинні бути завершені на нуль (вони мають вбудований розмір), і це дозволяє випадковим на місці сплайсингу довших рядків.


4
Що з низовинами? std::string_views - просто фантазія const char*s, період. GCC реалізує їх так:class basic_string_view {const _CharT* _M_str; size_t _M_len;}
n.caillou

4
просто перейдіть до 65K представників (з ваших нинішніх 65), і це буде прийнятою відповіддю (хвилі до натовпу культового натовпу) :)
mlvljr

7
@mlvljr Ніхто не проходить std::string const*. І ця діаграма незрозуміла. @ n.caillou: Ваш власний коментар вже точніший за відповідь. Це робить string_viewбільше, ніж "фантазії char const*" - це дійсно цілком очевидно.
177

@sehe Я міг би бути тим, що ніхто, ніяких проблем (тобто передача покажчика (або посилання) на рядок const, чому б і ні?) :)
mlvljr

2
@sehe Ви це розумієте з точки зору оптимізації чи виконання, std::string const*і std::string const&ви однакові, чи не так?
n.caillou
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.