web-dev-qa-db-ja.com

std :: string_viewはconst std :: string&と比べてどれだけ正確ですか?

std::string_view がC++ 17になったので、const std::string&の代わりにそれを使うことが広く推奨されています。

その理由の一つはパフォーマンスです。

誰かが正確にstd::string_viewがパラメータ型として使われるときconst std::string&よりどのくらい速いのかを説明できますか? (呼び出し先にコピーが作成されていないと仮定しましょう)

176
Patryk

std::string_viewはいくつかのケースではより速いです。

まず、std::string const&は、データが生のC配列ではなくstd::string、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を取った場合もあります。

2番目に大きな理由は、コピーなしで部分文字列を扱うことを許可していることです。 2ギガバイトのJSON文字列(!)²を解析しているとします。あなたがそれをstd::stringにパースするならば、それらがノードの名前または値を格納するそれぞれのそのようなパースノードコピー 2ギガバイト文字列からローカルノードへのオリジナルデータ。

代わりに、あなたがそれをstd::string_viewsにパースするならば、ノード参照は元のデータを指します。これにより、数百万の割り当てを節約し、解析中のメモリ要件を半分にすることができます。

あなたが得ることができるスピードアップは単にばかげている。

これは極端な場合ですが、他の「部分文字列を取得して処理する」場合もstring_viewを使用して適切なスピードアップを生成できます。

決定の重要な部分はstd::string_viewを使って失うものです。それほど多くはありませんが、それは何かです。

あなたは暗黙のヌル終了を失います、そしてそれはそれについてです。したがって、同じ文字列が3つの関数に渡され、そのすべてがNULLターミネータを必要とする場合は、一度std::stringに変換するのが賢明です。したがって、あなたのコードがnullターミネータを必要とすることがわかっていて、Cスタイルのソースバッファなどから供給される文字列を期待していないのなら、おそらくstd::string const&を使ってください。そうでなければstd::string_viewを取ります。

もしstd::string_viewがnullで終わっているかどうかを示すフラグを持っていたら(あるいはもっとおかしなことであれば)、それはstd::string const&を使う最後の理由でさえ削除します。

std::stringを付けずにconst&を取るのがstd::string_viewよりも最適である場合があります。呼び出し後に文字列のコピーを無期限に所有する必要がある場合は、by-valueを使用するのが効率的です。あなたはSBOの場合(そして割り当てなし、それを複製するための数文字のコピー)、あるいは移動ヒープ割り当てバッファをローカルのstd::stringに入れることができるでしょう。 2つのオーバーロードstd::string&&std::string_viewを持つことはより速いかもしれませんが、ほんのわずかです、そしてそれは適度なコード膨張を引き起こすでしょう(それはあなたにすべてのスピード向上を犠牲にするかもしれません)。


¹スモールバッファ最適化

²実際のユースケース。

176

String_viewがパフォーマンスを向上させる1つの方法は、プレフィックスとサフィックスを簡単に削除できることです。内部的には、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
50
Pavel Davydov

主な理由は2つあります。

  • string_viewは既存のバッファ内のスライスです。メモリの割り当ては不要です。
  • string_viewは参照ではなく値で渡されます

スライスを持つ利点は複数あります。

  • 新しいバッファを割り当てずにchar const*またはchar[]と一緒に使用できます。
  • 割り当てなくても、multipleスライスとサブスライスを既存のバッファに入れることができます。
  • 部分文字列はO(1)であり、O(N)ではありません
  • ...

より良い、そしてより一貫性のある全体的なパフォーマンス。


値による受け渡しは参照による受け渡しよりもエイリアスがあるので利点もあります。

特にstd::string const&パラメータがある場合、参照文字列が変更されないという保証はありません。結果として、コンパイラは各呼び出しの後に文字列の内容を不透明なメソッド(dataへのポインタ、長さ、...)に再フェッチしなければなりません。

一方、string_viewを値で渡すと、コンパイラは、スタック上(またはレジスタ内)にある長さおよびデータポインタを他のコードが変更できないことを静的に判断できます。結果として、関数呼び出し間でそれらを「キャッシュ」することができます。

43
Matthieu M.

それができることの1つは、NULLで終わる文字列からの暗黙の変換の場合に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.
36
juanchopanza