299

std::string_view已经到了 C++17 并且被广泛推荐使用它而不是const std::string&.

原因之一是性能。

有人可以解释一下究竟 std::string_view是/将比const std::string&用作参数类型时更快吗?(假设在被调用者中没有复制)

4

5 回答 5

276

std::string_view在少数情况下更快。

首先,std::string const&要求数据在 a 中std::string,而不是原始 C 数组、char const*由 C API 返回的 a、std::vector<char>由某些反序列化引擎生成的 a 等。避免的格式转换避免了复制字节,并且(如果字符串长于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,但会出现 iffoo采用 astd::string const&而不是 a string_view

第二个真正重要的原因是它允许在没有副本的情况下使用子字符串。假设您正在解析一个 2 GB 的 json 字符串 (!)²。如果将其解析为std::string,则每个存储节点名称或值的此类解析节点会将原始数据从 2 gb 字符串复制到本地节点。

相反,如果将其解析为std::string_views,则节点引用原始数据。这可以在解析期间节省数百万次分配并将内存需求减半。

您可以获得的加速简直是荒谬的。

这是一个极端情况,但其他“获取子字符串并使用它”的情况也可以通过string_view.

决定的一个重要部分是使用std::string_view. 这不是很多,但它是一些东西。

你失去了隐含的空终止,就是这样。因此,如果将相同的字符串传递给 3 个函数,所有这些函数都需要一个空终止符,那么转换为std::string一次可能是明智的。因此,如果您的代码已知需要一个空终止符,并且您不希望从 C 风格的源缓冲区等提供字符串,则可能需要一个std::string const&. 否则采取std::string_view.

如果std::string_view有一个标志表明它是否为空终止(或更高级的东西),它甚至会删除使用std::string const&.

有一种情况,在 a 上取 astd::string而不带 noconst&是最优的std::string_view。如果您需要在调用后无限期地拥有字符串的副本,则按值获取是有效的。您要么处于 SBO 的情况下(并且没有分配,只需几个字符副本即可复制它),要么您将能够堆分配的缓冲区移动到本地std::string. 有两个重载std::string&&std::string_view可能会更快,但只是轻微的,它会导致适度的代码膨胀(这可能会让你失去所有的速度增益)。


¹ 小缓冲区优化

² 实际用例。

于 2016-10-19T10:46:16.353 回答
81

string_view 提高性能的一种方法是它允许轻松删除前缀和后缀。在后台,string_view 可以将前缀大小添加到指向某个字符串缓冲区的指针,或者从字节计数器中减去后缀大小,这通常很快。另一方面,当您执行 substr 之类的操作时,std::string 必须复制其字节(这样您将获得一个拥有其缓冲区的新字符串,但在许多情况下您只想获取原始字符串的一部分而不复制)。例子:

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

更新:

我写了一个非常简单的基准来添加一些实数。我使用了很棒的谷歌基准库。基准函数是:

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
于 2016-10-19T10:40:10.633 回答
59

有两个主要原因:

  • string_view是现有缓冲区中的一个切片,它不需要内存分配
  • string_view按值传递,而不是按引用传递

切片的优点是多方面的:

  • char const*您可以在分配或char[]不分配新缓冲区的情况下使用它
  • 您可以将多个切片和子切片放入现有缓冲区而无需分配
  • 子串是 O(1),而不是 O(N)
  • ...

更好、更一致的性能。


通过值传递也比通过引用传递具有优势,因为别名。

具体来说,当你有一个std::string const&参数时,不能保证引用字符串不会被修改。因此,编译器必须在每次调用不透明方法(指向数据、长度……)之后重新获取字符串的内容。

另一方面,当string_view按值传递 a 时,编译器可以静态确定没有其他代码可以修改现在堆栈(或寄存器)中的长度和数据指针。因此,它可以跨函数调用“缓存”它们。

于 2016-10-19T11:19:07.143 回答
40

它可以做的一件事是避免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.
于 2016-10-19T09:58:08.987 回答
12

std::string_view基本上只是一个const char*. 并且传递const char*意味着与传递const string*(或const string&)相比,系统中的指针会少一个,因为这string*意味着:

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

显然,为了传递 const 参数,第一个指针是多余的。

psstd::string_view和之间的一个实质性区别const char*是 string_views 不需要以空值结尾(它们具有内置大小),这允许随机就地拼接较长的字符串。

于 2016-10-20T00:44:12.087 回答