为什么 std: : string_view 比 const std: : string & 快?

std::string_view 已经进入 C + + 17,广泛推荐使用它而不是 const std::string&

其中一个原因是性能。

有人能解释一下 没错 std::string_view作为参数类型时是如何快于 const std::string&的吗?(假设被调用方中没有复制)

75729 次浏览

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

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_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情况下没有分配,但如果foostd::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有一个标志,声明它是否以空结束(或其他更奇特的东西),它甚至会删除使用std::string const&的最后一个原因。

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


¹小缓冲区优化

²实际用例。

主要有两个原因:

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

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

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

更好的更加一致性能。


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

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

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

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

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

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

std::string_viewconst char*之间的一个本质区别是,string_views不需要以空结束(它们有内置大小),这允许随机在原地拼接较长的字符串。