std::string_view 究竟比 const std::string& 快多少?

How exactly is std::string_view faster than const std::string&?

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

其中一个原因是性能。

有人可以解释一下 exactly std::string_view is/will 用作参数类型时如何比 const std::string& 快吗? (假设被调用者没有复制)

它可以做的一件事是避免在从空终止字符串进行隐式转换的情况下构造 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");

更新:

我写了一个非常简单的基准来添加一些实数。我用了真棒google benchmark library。基准函数是:

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 的情况下没有分配,但是如果 foo 使用 std::string const& 而不是 string_view

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

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

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

这是一个极端案例,但其他 "get a substring and work with it" 案例也可以通过 string_view 产生不错的加速。

决定的一个重要部分是使用 std::string_view 会失去什么。不多,但也有。

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

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

的最后一个理由

有一种情况是 std::string 而没有 const& 优于 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 时,编译器可以静态地确定没有其他代码可以修改堆栈(或寄存器)中的长度和数据指针。因此,它可以跨函数调用 "cache" 它们。

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

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

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

p.s. std::string_viewconst char* 之间的一个本质区别是 string_views 不是必需的以 null 结尾(它们具有内置大小),这允许对较长的字符串进行随机就地拼接。