Как именно std :: string_view быстрее, чем const std :: string &?

221

std::string_viewсделал это в C ++ 17 и широко рекомендуется использовать его вместо const std::string&.

Одна из причин - производительность.

Может кто-нибудь объяснить, как именно std::string_view / будет быстрее, чем const std::string&при использовании в качестве типа параметра? (давайте предположим, что в вызываемом номере не сделано ни одной копии)

Patryk
источник
7
std::string_viewэто просто абстракция пары (char * begin, char * end). Вы используете его при создании std::stringненужной копии.
Вопрос
На мой взгляд, вопрос не в том, какой из них быстрее, а в том, когда их использовать. Если мне нужны некоторые манипуляции со строкой, которые не являются постоянными и / или сохраняют исходное значение, string_view идеально подходит, потому что мне не нужно делать копию строки для него. Но если мне нужно только проверить что-то в строке, используя, например, string :: find, тогда ссылка лучше.
TheArquitect
@QuestionC вы используете его, когда не хотите, чтобы ваш API ограничивался std::string(string_view может принимать необработанные массивы, векторы, std::basic_string<>с распределителями, отличными от заданных по умолчанию и т. Д. И т. Д. И т. Д. Да, и другие строковые_виды, очевидно)
sehe

Ответы:

213

std::string_view быстрее в нескольких случаях.

Во-первых, std::string const&требуется , чтобы данные находились в std::stringмассиве C, а не в необработанном массиве C, char const*возвращались API C, 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&является оптимальным по сравнению с std::string_view. Если вам нужно владеть копией строки на неопределенный срок после вызова, эффективный захват по значению. Вы будете либо в случае SBO (и без выделения, всего несколько копий символов для его дублирования), либо вы сможете переместить выделенный в куче буфер в локальный std::string. Имея две перегрузки std::string&&и std::string_viewможет быть быстрее, но лишь незначительно, и это вызвало бы скромное разрастание кода (который может стоить вам все преимущества скорости).


¹ Оптимизация малого буфера

² Фактический вариант использования.

Якк - Адам Невраумонт
источник
8
Вы также теряете право собственности. Который представляет интерес только в том случае, если возвращается строка, и это может быть что угодно, кроме подстроки буфера, который гарантированно просуществует достаточно долго. На самом деле, потеря собственности - очень обоюдоострое оружие.
Дедупликатор
SBO звучит странно. Я всегда слышал 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.
juanchopanza
источник
12
Возможно, стоит сказать, что, const std::string str{"goodbye!"}; foo(str);вероятно , не будет быстрее с string_view, чем с string &
Мартин Боннер поддерживает Monica
1
Разве не string_viewбудет медленным, поскольку он должен копировать два указателя, а не один указатель в const string&?
Балки
9

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

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

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

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

n.caillou
источник
4
Что с отрицательными голосами? std::string_viewс просто фантазии const char*, точка. GCC реализует их так:class basic_string_view {const _CharT* _M_str; size_t _M_len;}
n.caillou
4
просто доберитесь до 65 тыс. представителей (от ваших нынешних 65), и это будет принятый ответ (
махает
7
@mlvljr Никто не проходит std::string const*. И эта диаграмма непонятна. @ n.caillou: Ваш собственный комментарий уже более точен, чем ответ. Это делает string_viewбольше, чем просто фантазию char const*- это действительно очевидно.
Се
@ я мог бы быть тем, что никто, без проблем (т.е. передача указателя (или ссылки) на константную строку, почему бы и нет?) :)
mlvljr
2
@sehe Вы понимаете это с точки зрения оптимизации или исполнения, std::string const*и std::string const&так же, не так ли?
n.caillou