std::string_view已经加入到c++ 17中,广泛建议使用它来代替const std::string&。

其中一个原因是性能。

有人能解释一下std::string_view作为参数类型使用时,到底是/将比const std::string&更快吗?(让我们假设在被调用者中没有复制)


当前回答

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

更新:

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

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

其他回答

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

更新:

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

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

它能做的一件事是避免构造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.

Std::string_view在某些情况下更快。

首先,std::string const&要求数据是一个std::string,而不是一个原始的C数组,一个由C API返回的char const*,一个由某些反序列化引擎产生的std::vector<char>,等等。避免的格式转换避免了复制字节,并且(如果字符串长于特定std::string实现的SBO¹)避免了内存分配。

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,就会有分配。

第二个真正重要的原因是,它允许在没有副本的情况下处理子字符串。假设你正在解析一个2gb的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有一个标志,说明它是否以null结尾(或其他更花哨的东西),它甚至会删除使用std::string const&的最后一个原因。

There is a case where taking a std::string with no const& is optimal over a std::string_view. If you need to own a copy of the string indefinitely after the call, taking by-value is efficient. You'll either be in the SBO case (and no allocations, just a few character copies to duplicate it), or you'll be able to move the heap-allocated buffer into a local std::string. Having two overloads std::string&& and std::string_view might be faster, but only marginally, and it would cause modest code bloat (which could cost you all of the speed gains).


¹小缓冲区优化

²实际用例。

主要有两个原因:

String_view是现有缓冲区中的一个片,它不需要内存分配 String_view是通过值传递的,而不是通过引用


有一片的好处是多方面的:

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

更好更稳定的表现。


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

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

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

Std::string_view基本上只是对const char*的包装。传递const char*意味着与传递const string&(或const string*)相比,系统中会少一个指针,这意味着类似于:

string* -> char* -> char[]
           (   string    )

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

p.s. .尽管如此,std::string_view和const char*之间的一个实质性区别是,string_views不需要以空结束(它们有内置大小),这允许随机地在原地拼接较长的字符串。