std::string_view
がC++ 17になったので、const std::string&
の代わりにそれを使うことが広く推奨されています。
その理由の一つはパフォーマンスです。
誰かが正確にstd::string_view
がパラメータ型として使われるときconst std::string&
よりどのくらい速いのかを説明できますか? (呼び出し先にコピーが作成されていないと仮定しましょう)
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
の場合、割り当ては行われませんが、foo
がstd::string const&
ではなくstring_view
を取った場合もあります。
2番目に大きな理由は、コピーなしで部分文字列を扱うことを許可していることです。 2ギガバイトのJSON文字列(!)²を解析しているとします。あなたがそれをstd::string
にパースするならば、それらがノードの名前または値を格納するそれぞれのそのようなパースノードコピー 2ギガバイト文字列からローカルノードへのオリジナルデータ。
代わりに、あなたがそれをstd::string_view
sにパースするならば、ノード参照は元のデータを指します。これにより、数百万の割り当てを節約し、解析中のメモリ要件を半分にすることができます。
あなたが得ることができるスピードアップは単にばかげている。
これは極端な場合ですが、他の「部分文字列を取得して処理する」場合も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
を持つことはより速いかもしれませんが、ほんのわずかです、そしてそれは適度なコード膨張を引き起こすでしょう(それはあなたにすべてのスピード向上を犠牲にするかもしれません)。
¹スモールバッファ最適化
²実際のユースケース。
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
主な理由は2つあります。
string_view
は既存のバッファ内のスライスです。メモリの割り当ては不要です。string_view
は参照ではなく値で渡されますスライスを持つ利点は複数あります。
char const*
またはchar[]
と一緒に使用できます。より良い、そしてより一貫性のある全体的なパフォーマンス。
値による受け渡しは参照による受け渡しよりもエイリアスがあるので利点もあります。
特にstd::string const&
パラメータがある場合、参照文字列が変更されないという保証はありません。結果として、コンパイラは各呼び出しの後に文字列の内容を不透明なメソッド(dataへのポインタ、長さ、...)に再フェッチしなければなりません。
一方、string_view
を値で渡すと、コンパイラは、スタック上(またはレジスタ内)にある長さおよびデータポインタを他のコードが変更できないことを静的に判断できます。結果として、関数呼び出し間でそれらを「キャッシュ」することができます。
それができることの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.