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

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

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

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

Ответ 1

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

Во-первых, std::string const& требует, чтобы данные находились в std::string, а не как исходный массив C, a char const*, возвращаемый API C, a 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, каждый такой синтаксис node, где они хранят имя или значение node, копирует исходные данные из строки 2 gb в локальный node.

Вместо этого, если вы проанализируете его на std::string_view s, узлы ссылаются на исходные данные. Это может сэкономить миллионы ассигнований и сократить вдвое потребности в памяти при разборе.

Ускорение, которое вы можете получить, просто смешно.

Это крайний случай, но другие "получают подстроку и работают с ним", случаи также могут генерировать приличные ускорения с помощью string_view.

Важной частью решения является то, что вы теряете, используя std::string_view. Это не много, но это что-то.

Вы теряете неявное завершение нулевого значения, и об этом. Поэтому, если одна и та же строка будет передана в 3 функции, все из которых требуют нулевого терминатора, однократное преобразование в std::string может быть разумным. Таким образом, если вашему коду, как известно, нужен нулевой терминатор, и вы не ожидаете, что строки, поданные из буферов с исходным кодом C или т.п., возможно, возьмут 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 может быть более быстрым, но только незначительно, и это вызовет размытие кода (которое может стоить вам всех приростов скорости).


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

² Фактический прецедент.

Ответ 2

Один из способов, с помощью которого 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");

Обновление:

Я написал очень простой тест, чтобы добавить некоторые реальные цифры. Я использовал awesome библиотеку тестов 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

Ответ 3

Есть две основные причины:

  • string_view - это срез в существующем буфере, для него не требуется выделение памяти
  • string_view передается по значению, а не по ссылке

Преимущества наличия среза несколько:

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

Лучшая и более стабильная производительность на всем протяжении.


Передача по значению также имеет преимущества перед передачей по ссылке, поскольку псевдонимы.

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

С другой стороны, при передаче значения string_view по значению компилятор может статически определять, что ни один другой код не может изменять указатели длины и данных в стеке (или в регистре). В результате он может "кэшировать" их через вызовы функций.

Ответ 4

Единственное, что он может сделать, это не конструировать объект 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.

Ответ 5

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

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

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

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