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

374

std::string_view 已经进入C++17,并且广泛推荐使用它来代替 const std::string&

原因之一是性能。

有人可以解释一下当作为参数类型时,std::string_viewconst std::string&相比究竟如何更快?(假设在被调用函数中没有进行复制)


17
std::string_view只是(char* begin, char* end)这一对的抽象表示。使用它可以避免不必要的复制,通常在创建std::string会产生副本时使用。 - QuestionC
1
在我看来,问题并不是哪个更快,而是何时使用它们。如果我需要对字符串进行一些操作,而且这些操作不是永久性的和/或保留原始值,那么string_view就非常完美,因为我不需要将字符串复制到它上面。但是,如果我只需要使用string::find之类的方法检查字符串中的某些内容,那么引用更好。 - TheArchitect
2
@QuestionC 是在不想把你的 API 限制为 std::string 时使用的(string_view 可以接受原始数组、向量、带有非默认分配器的 std::basic_string<> 等等等等。哦,当然还有其他的 string_view)。 - sehe
6个回答

342

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] );
}

如果 foo 接受的不是 string_view 而是 std::string const&,那么在 string_view 情况下就没有分配内存。

第二个非常重要的原因是,它允许在不复制的情况下处理子字符串。假设您正在解析一个 2GB 的 JSON 字符串!² 如果将其解析为 std::string,则每个此类解析节点,在其中存储节点的名称或值时,都会将原始数据从 2GB 的字符串复制到本地节点。

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

你可以获得的加速效果就是简直令人难以置信。

这是一个极端案例,但其他“获取子字符串并处理它”的情况也可以使用 string_view 产生相当大的速度提升。

做出决策的一个重要部分是使用 std::string_view 所失去的东西。虽然不多,但确实存在。

你失去了隐式的 null 终止符,就这样而已。因此,如果将相同的字符串传递给需要 null 终止符的 3 个函数,可能明智的做法是先转换为 std::string。 因此,如果您的代码需要 null 终止符,并且不希望从类似 C 样式缓冲区中提供的字符串,则可以采用 std::string const&。否则使用 std::string_view

如果 std::string_view 具有指示其是否以 null 结尾(或更高级别)的标志,则甚至可以消除使用 std::string const& 的最后一个原因。

有一种情况,在这种情况下,采用没有 const&std::string 对于 std::string_view 是最佳的选择。 如果需要在调用后无限期拥有字符串的副本,则按值获取是有效的。 您将处于 SBO 案例中(没有分配,只需复制几个字符即可复制它),或者您将能够将堆分配的缓冲区 “移动” 到本地的 std::string 中。 有两个重载 std::string&&std::string_view 可能更快,但只是轻微的,并且会导致适度的代码膨胀(这可能会导致您失去所有速度增益)。


¹ 小缓冲区优化

² 实际用例。


21
如果返回的字符串可能不仅是缓冲区的子字符串,并且需要保证其存活时间足够长,那么您也会失去所有权。实际上,失去所有权是一把非常双刃剑。 - Deduplicator
8
SBO听起来很奇怪。我一直听说的是SSO(小字符串优化)。 - phuclv
1
@phuclv SSO只是SBO的一个特定案例,SBO代表_small buffer optimization_。替代术语为_small data opt._、_small object opt._或_small size opt._。 - Daniel Langr
2
顺便提一下,char const*const* argv 只是展示了 C++ 代码中愚蠢和歧义可以达到的程度... - digito_evo
1
@digito 那个有什么不明确的? - Yakk - Adam Nevraumont
显示剩余7条评论

102

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

8
你提供了一个实际的基准测试,这非常好。这真正展示了在相关应用场景中可以得到多少收益。 - Daniel Kamil Kozar
1
@DanielKamilKozar 感谢您的反馈。我也认为基准测试非常有价值,有时候它们可以改变一切。 - Pavel Davydov

67
有两个主要原因:
  • string_view是现有缓冲区中的一个切片,它不需要进行内存分配。
  • string_view是通过值传递,而非引用传递。
具有切片的优点如下:
  • 您可以将其与char const*char[]一起使用,而无需分配新缓冲区。
  • 您可以在现有缓冲区中获取多个切片和子切片,而无需进行分配。
  • substring的时间复杂度为O(1),而不是O(N)。
  • ......
更好和更加一致的性能表现。
通过值传递也优于通过引用传递,因为存在别名问题。
具体来说,当您拥有一个std::string const&参数时,没有保证引用字符串不会被修改。因此,在调用不透明方法(指向数据的指针、长度等)后,编译器必须重新获取字符串的内容。
另一方面,当通过值传递string_view时,编译器可以静态地确定现在在堆栈(或寄存器)上的长度和数据指针不会被其他代码修改。因此,它可以在函数调用之间“缓存”它们。

43

它能够做的一件事情是,在从一个空终止的字符串进行隐式转换的情况下,避免构造一个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.

14
可能值得一提的是,const std::string str{"goodbye!"}; foo(str);string_view 和用 string& 相比,可能并没有更快的速度。 - Martin Bonner supports Monica
3
string_view 需要复制两个指针,而 const string& 只需要复制一个指针,那么它不会变得更慢吗? - balki
1
@balki:理论上,传递两个指针(或一个指针和一个size_t)可能会稍微慢一点,但是换取的是使用string_view更快;要找到std::string的第一个字符,它需要跟随引用到某个非固定、可能非本地的位置,然后跟随指针到字符串开头找到它。而对于string_view,它可以直接跟随指针到视图的开头。为了在前面复制两个指针,您可以从每个使用中删除一级间接性。 - ShadowRanger

15

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

std::string_view基本上就像是围绕着一个const char*的外壳。传递const char*表示,在与传递const string&(或const string*)相比时,系统中将少一个指针,意味着:

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

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

p.s. 然而,std::string_viewconst char*之间一个重要的区别是,string_views不需要以null结尾(它们有内置大小),这允许更长字符串的随机原地接合。


4
为什么会有踩票?std::string_view只不过是比较时髦的const char*而已,就这样。GCC实现它的方式是:class basic_string_view {const _CharT* _M_str; size_t _M_len;} - n.caillou
5
只需将您的声望值提升到65K(从当前的65起),这将成为被接受的答案(向模仿者致意) :) - mlvljr
9
@mlvljr: 没有人传递 std::string const*。而且那个图解很难理解。@n.caillou: 你自己的评论已经比答案更准确了。这使得 string_view 不仅仅是一个“花哨的 char const*”,这是非常明显的。 - sehe
4
你知道从优化或执行的角度来看,std::string const*std::string const&是相同的吗?请注意,这里只需要翻译,不需要解释或添加其他内容。 - n.caillou
1
从便利性和程序员安全的角度来看,const 引用具有执行临时变量隐式保持活动状态职责(指针无法做到)并防止(非病态的)传递 nullptr 的能力,这是为什么没有人使用 std::string const* 的主要原因之一。 - ShadowRanger
显示剩余4条评论

-1

这里没有任何答案。问题除了已经构造好的字符串通过const引用和字符串视图被传递之外,没有提供任何其他信息。我认为我们不应该讨论其他情况。在这种特定情况下,我并没有看到任何区别,因为即使是问问题的人明确指定(假设调用方没有进行拷贝),讨论前缀等操作都不是问题的焦点...

也许我漏掉了什么,但就算有区别,差距也非常微小。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接