【问题标题】:How exactly is std::string_view faster than const std::string&?std::string_view 到底比 const std::string& 快多少?
【发布时间】:2017-02-28 21:23:49
【问题描述】:

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

其中一个原因是性能。

有人可以确切地解释std::string_view 在用作参数类型时比const std::string& 更快吗? (假设在被调用者中没有复制)

【问题讨论】:

  • std::string_view 只是 (char * begin, char * end) 对的抽象。您在制作 std::string 时使用它是不必要的副本。
  • 在我看来,问题不在于哪一个更快,而在于何时使用它们。如果我需要对字符串进行一些操作并且它不是永久性的和/或保留原始值,那么 string_view 是完美的,因为我不需要为它制作字符串的副本。但是,如果我只需要使用 string::find 来检查字符串上的某些内容,那么引用会更好。
  • @QuestionC 当您不希望您的 API 限制为 std::string 时使用它(string_view 可以接受原始数组、向量、std::basic_string<> 和非默认分配器等等等等。哦,显然还有其他 string_views)

标签: c++ string c++17 string-view


【解决方案1】:

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

【讨论】:

  • 值得一提的是,const std::string str{"goodbye!"}; foo(str); 可能不会使用 string_view 比使用 string& 更快
  • 不会很慢 string_view 因为它必须复制两个指针而不是 const string& 中的一个指针?
【解决方案2】:

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

更新:

我写了一个非常简单的基准来添加一些实数。我用了很棒的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

【讨论】:

  • 很高兴您提供了一个实际的基准。这确实显示了在相关用例中可以获得什么。
  • @DanielKamilKozar 感谢您的反馈。我也认为基准很有价值,有时它们会改变一切。
【解决方案3】:

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&amp; 而不是string_view,则会出现这种情况。

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

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

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

这是一个极端情况,但其他“获取子字符串并使用它”的情况也可以通过string_view 产生不错的加速。

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

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

如果std::string_view 有一个标志,表明它是否为空终止(或更奇特的东西),它甚至会删除使用std::string const&amp; 的最后一个理由。

在某些情况下,采用不带 const&amp;std::string 优于 std::string_view。如果您需要在调用后无限期地拥有字符串的副本,则按值获取是有效的。您要么处于 SBO 的情况下(并且没有分配,只需几个字符副本即可复制它),要么您可以将堆分配的缓冲区 移动 到本地 std::string .有两个重载 std::string&amp;&amp;std::string_view 可能会更快,但只是很小,它会导致适度的代码膨胀(这可能会让你失去所有的速度提升)。


¹小缓冲区优化

² 实际用例。

【讨论】:

  • 您也失去了所有权。仅当返回字符串并且它可能必须是缓冲区的子字符串之外的任何内容时才有意义,该子字符串保证可以存活足够长的时间。实际上,失去所有权是一把非常两刃的武器。
  • SBO 听起来很奇怪。一直听说 SSO(小字符串优化)
  • @phu 当然;但是字符串并不是你唯一使用这个技巧的东西。
  • @phuclv SSO 只是 SBO 的一个特例,它代表 small buffer optimization。替代术语是 small data opt.small object opt.small size opt..
  • 作为旁注,char const*const* argv 只是展示了 C++ 代码中的愚蠢和歧义...
【解决方案4】:

有两个主要原因:

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

切片的好处是多方面的:

  • 您可以将其与 char const*char[] 一起使用,而无需分配新缓冲区
  • 您可以将多个切片和子切片放入现有缓冲区,而无需分配
  • 子字符串是 O(1),而不是 O(N)
  • ...

整体性能更好,更一致


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

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

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

【讨论】:

    【解决方案5】:

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

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

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

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

    【讨论】:

    • 反对票是怎么回事? std::string_views 只是花哨的const char*s,期间。 GCC 像这样实现它们:class basic_string_view {const _CharT* _M_str; size_t _M_len;}
    • 只要达到 65K 代表(从你目前的 65 开始),这将是公认的答案(向货物狂热的人群挥手):)
    • @mlvljr 没有人通过std::string const*。那张图是难以理解的。 @n.caillou:你自己的评论已经比答案更准确了。这使得string_view 比“花哨的char const*”更重要——这真的很明显。
    • @sehe 我可能是没有人,没问题(即将指针(或引用)传递给 const 字符串,为什么不呢?):)
    • @sehe 你明白,从优化或执行的角度来看,std::string const*std::string const&amp; 是一样的,不是吗?
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-12-29
    • 1970-01-01
    • 2020-03-24
    • 1970-01-01
    相关资源
    最近更新 更多