web-dev-qa-db-ja.com

C ++での効率的な文字列連結

Std :: stringの「+」演算子と、連結を高速化するためのさまざまな回避策についての懸念を表明する人が数人います。これらのどれかが本当に必要ですか?もしそうなら、C++で文字列を連結する最良の方法は何ですか?

94
sneg

実際に効率が必要な場合を除き、余分な作業はおそらく価値がありません。代わりに演算子+ =を使用するだけで、効率が大幅に向上します。

今、その免責事項の後、私はあなたの実際の質問に答えます...

STL文字列クラスの効率は、使用しているSTLの実装に依存します。

C組み込み関数を使用して手動で連結を行うことにより、効率を保証するおよびより強力に制御することができます。

operator +が効率的でない理由:

このインターフェースを見てください:

template <class charT, class traits, class Alloc>
basic_string<charT, traits, Alloc>
operator+(const basic_string<charT, traits, Alloc>& s1,
          const basic_string<charT, traits, Alloc>& s2)

それぞれの+の後に新しいオブジェクトが返されることがわかります。これは、毎回新しいバッファーが使用されることを意味します。大量の余分な+操作を行う場合、効率的ではありません。

なぜもっと効率的にできるのか:

  • デリゲートを信頼して効率的にそれを行うのではなく、効率を保証します
  • std :: stringクラスは、文字列の最大サイズについても、連結する頻度についても何も知りません。あなたはこの知識を持っているかもしれず、この情報を持っていることに基づいて物事をすることができます。これにより、再割り当てが少なくなります。
  • バッファを手動で制御するので、文字列全体を新しいバッファにコピーしたくない場合は、それを行わないようにすることができます。
  • より効率的なヒープの代わりに、バッファにスタックを使用できます。
  • 文字列+演算子は、新しい文字列オブジェクトを作成し、新しいバッファを使用してそれを返します。

実装に関する考慮事項:

  • 文字列の長さを追跡します。
  • 文字列の終わりと開始点、または単に開始点へのポインタを保持し、開始点と長さをオフセットとして使用して、文字列の終わりを見つけます。
  • 文字列を保存するバッファが十分に大きいことを確認して、データを再割り当てする必要がないようにします
  • 文字列の最後を見つけるために文字列の長さを反復処理する必要がないように、strcatの代わりにstrcpyを使用します。

ロープのデータ構造:

本当に高速な連結が必要な場合は、 ロープデータ構造 の使用を検討してください。

83
Brian R. Bondy

前に最終スペースを確保してから、バッファでappendメソッドを使用します。たとえば、最終的な文字列の長さが100万文字になると予想するとします。

std::string s;
s.reserve(1000000);

while (whatever)
{
  s.append(buf,len);
}
71

私はそれを心配しません。ループで実行すると、文字列は常にメモリを事前に割り当てて再割り当てを最小限に抑えます。その場合はoperator+=を使用します。そして、あなたが手動でそれを行う場合、これ以上のようなもの

a + " : " + c

次に、一時的なものを作成します-コンパイラが戻り値のコピーを削除できたとしても。これは、連続して呼び出されるoperator+では、参照パラメーターが名前付きオブジェクトを参照するか、sub operator+呼び出しから返される一時的なものを参照するかがわからないためです。最初にプロファイルを作成する前に、心配する必要はありません。しかし、それを示すための例を取り上げましょう。最初に括弧を導入して、バインディングを明確にします。明確にするために使用される関数宣言の直後に引数を配置します。その下に、結果の式が何であるかを示します:

((a + " : ") + c) 
calls string operator+(string const&, char const*)(a, " : ")
  => (tmp1 + c)

さて、その追加で、tmp1は、示された引数でoperator +を最初に呼び出したときに返されたものです。コンパイラは本当に賢く、戻り値のコピーを最適化するものと想定しています。したがって、a" : "の連結を含む1つの新しい文字列になります。今、これが起こります:

(tmp1 + c)
calls string operator+(string const&, string const&)(tmp1, c)
  => tmp2 == <end result>

以下と比較してください。

std::string f = "hello";
(f + c)
calls string operator+(string const&, string const&)(f, c)
  => tmp1 == <end result>

一時的なものと名前付きの文字列に対して同じ関数を使用しています!したがって、コンパイラhasは、引数を新しい文字列にコピーし、それに追加して、operator+の本体から返します。一時のメモリを取得して追加することはできません。式が大きいほど、より多くの文字列のコピーを作成する必要があります。

次のVisual StudioとGCCは、c ++ 1xの移動セマンティクス(補完コピーセマンティクス)と実験的な追加としての右辺値参照をサポートします。これにより、パラメーターが一時を参照しているかどうかを判断できます。上記のすべてがコピーなしの1つの「追加パイプライン」になるため、これによりこのような追加が驚くほど高速になります。

それがボトルネックであることが判明した場合、あなたはまだ行うことができます

 std::string(a).append(" : ").append(c) ...

append呼び出しは、引数を*thisに追加し、それ自体への参照を返します。したがって、そこで一時的なコピーは行われません。または、operator+=を使用できますが、優先順位を修正するにはfixい括弧が必要です。

ほとんどのアプリケーションでは、それは重要ではありません。コードを書くだけで、+演算子が正確にどのように機能するかをまったく気にせず、問題が明らかなボトルネックになった場合にのみ問題を自分の手に渡してください。

11
Pesto

.NET System.Stringとは異なり、C++ std :: strings are mutableです。したがって、他のメソッドと同じ速さで単純な連結を介して構築できます。

7
James Curran

代わりにおそらくstd :: stringstream?

しかし、私はあなたがおそらくそれを保守可能で理解しやすい状態に保ち、あなたが本当に問題を抱えているかどうかを確認するべきだという感情に同意します。

5
Tim

Imperfect C++で、Matthew Wilsonはdynamic文字列連結器を提示します。これは、すべての部分を連結する前に1つの割り当てのみを行うために最終文字列の長さを事前計算します。 expression templatesを使用して、静的な連結子を実装することもできます。

この種のアイデアは、STLport std :: string実装で実装されています。これは、この正確なハックのために標準に準拠していません。

4
Luc Hermitte

std::stringoperator+は、新しい文字列を割り当てて、2つのオペランド文字列を毎回コピーします。何回も繰り返すと、高価になります、O(n)。

std::stringappendおよびoperator+=一方、文字列を大きくする必要があるたびに容量を50%増やします。これにより、メモリ割り当てとコピー操作の数が大幅に削減されます、O(log n)。

3
timmerov

小さな文字列の場合、それは重要ではありません。大きな文字列がある場合は、ベクターまたはその他のコレクションとしてパーツとして保存することをお勧めします。そして、1つの大きな文字列ではなく、そのようなデータのセットで動作するようにアルゴリズムを追加します。

複雑な連結にはstd :: ostringstreamが好きです。

2
Mykola Golubyev

ほとんどの場合と同様に、何かをするよりもしないほうが簡単です。

大きな文字列をGUIに出力したい場合、出力するものは何でも大きな文字列よりも文字列を細かく処理できることがあります(たとえば、テキストエディタでテキストを連結する-通常は行を別々に保ちます)構造体)。

ファイルに出力する場合は、大きな文字列を作成して出力するのではなく、データをストリーミングします。

遅いコードから不要な連結を削除した場合、連結を高速化する必要性を発見したことはありません。

2
Pete Kirkham

結果の文字列にスペースを事前に割り当てる(確保する)場合、おそらく最高のパフォーマンスが得られます。

template<typename... Args>
std::string concat(Args const&... args)
{
    size_t len = 0;
    for (auto s : {args...})  len += strlen(s);

    std::string result;
    result.reserve(len);    // <--- preallocate result
    for (auto s : {args...})  result += s;
    return result;
}

使用法:

std::string merged = concat("This ", "is ", "a ", "test!");
0
LanDenLabs

配列のサイズと割り当てられたバイト数を追跡​​するクラスにカプセル化された単純な文字の配列が最速です。

トリックは、開始時に1つの大きな割り当てを行うことです。

https://github.com/pedro-vicente/table-string

ベンチマーク

Visual Studio 2015、x86デバッグビルド、C++ std :: stringを大幅に改善。

| API                   | Seconds           
| ----------------------|----| 
| SDS                   | 19 |  
| std::string           | 11 |  
| std::string (reserve) | 9  |  
| table_str_t           | 1  |  
0
Pedro Vicente