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


108

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


13
基本的に、+は連結演算子ではありません(新しい文字列を生成するため)。+ =を使用して連結します。
マーティンヨーク、

1
C ++ 11以降、重要な点があります。operator+は、そのオペランドの1つを変更し、そのオペランドが右辺値参照によって渡された場合、移動によって返すことができます。libstdc++ たとえば、これを行います。したがって、operator +を一時的に呼び出すと、パフォーマンスがほぼ同じになる可能性があります。ボトルネックであることを示すベンチマークがない限り、読みやすさのために、デフォルトに設定することを支持する引数となる可能性があります。ただし、標準化された可変部分append()は最適読みやすいものになります...
underscore_d 2017年

回答:


85

本当に効率が必要でない限り、追加の作業はおそらくそれだけの価値はありません。 代わりに演算子+ =を使用するだけで、効率が大幅に向上するでしょう。

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

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

実装に関する考慮事項:

  • ストリングの長さを追跡します。
  • 文字列の末尾と先頭へのポインタを保持するか、先頭のみを指定し、start +長さをオフセットとして使用して、文字列の末尾を見つけます。
  • 文字列を格納しているバッファが、データを再割り当てする必要がないように十分大きいことを確認してください
  • strcatの代わりにstrcpyを使用すると、文字列の終わりを見つけるために文字列の長さを繰り返し処理する必要がなくなります。

ロープのデータ構造:

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


6
注:「STL」とは、元々HPが作成した完全に独立したオープンソースライブラリを指し、一部はISO標準C ++ライブラリの一部の基礎として使用されていました。。「のstd ::文字列は、」しかし、それは一緒に「STLと『文字列』への言及は完全に間違っているので、HPのSTLの一部ではなかった
ジェームズ・カラン

1
STLとstringを一緒に使用するのは間違っているとは言えません。sgi.com/tech/stl/table_of_contents.htmlを
ブライアンR.ボンディ

1
SGIがHPからSTLのメンテナンスを引き継いだとき、それは標準ライブラリと一致するように改造されました(そのため、私は「HPのSTLの一部ではない」と述べました)。それにもかかわらず、std :: stringの作成者はISO C ++委員会です。
James Curran

2
補足:長年にわたってSTLの維持を担当していたSGIの従業員は、ISO C ++標準化委員会のライブラリサブグループを率いるMatt Austernでした。
James Curran、

4
より明確なヒープではなく、バッファにスタックを使用できる理由を明確にするか、いくつかのポイントを教えてください。?この効率の違いはどこから来るのですか?
h7r 2013年

76

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

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

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

17

気にしない。ループでそれを行う場合、文字列は常にメモリを事前に割り当てて再割り当てを最小限に抑えます- operator+=その場合にのみ使用してください。そして、手動で行う場合は、このようなまたはより長いもの

a + " : " + c

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

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

さて、その追加tmp1で、示されている引数を使用したoperator +への最初の呼び出しによって返されたものです。コンパイラーは本当に賢く、戻り値のコピーを最適化することを想定しています。したがって、aand の連結を含む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>

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

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

それがボトルネックであることが判明した場合でも、

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

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


stdlibインプリメンターが実際にこれを行うことを確認する必要がありました。:P libstdc++operator+(string const& lhs, string&& rhs)return std::move(rhs.insert(0, lhs))ます。次に、両方が一時的なものであるoperator+(string&& lhs, string&& rhs)場合、lhs十分な容量が利用可能な場合は直接になりappend()ます。十分な容量がないoperator+=場合よりも速度が遅くなるリスクがあると私が考えるところlhs、それはにフォールバックするためrhs.insert(0, lhs)、バッファーを拡張し、のようappend()に新しいコンテンツを追加するだけでなく、rhsrightの元のコンテンツに沿ってシフトする必要もあります。
underscore_d 2017年

と比較したもう1つのオーバーヘッドoperator+=operator+、値を返す必要があるため、move()追加されたオペランドのいずれかである必要があることです。それでも、文字列全体をディープコピーするのに比べると、オーバーヘッドはかなり小さい(2、3のポインター/サイズをコピーする)と思います。
underscore_d 2017年

11

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


7
もちろん、ほとんどの場合それは価値がありませんが、これは実際には彼の質問に答えません。
ブライアンR.ボンディ

1
ええ。私は「プロファイルしてから最適化する」という質問にコメントとして入れることができると同意します:)
Johannes Schaub-litb 2009年

6
技術的には、これらは「必要」かどうかを尋ねました。彼らはそうではありません、そしてこれはその質問に答えます。
サマンサブラナム

十分に公平ですが、一部のアプリケーションでは間違いなく必要です。したがって、これらのアプリケーションでは、答えは次のようになります。「問題を自分の手に」
ブライアンR.ボンディ

4
@ペストプログラミングの世界では、パフォーマンスは重要ではないという変な概念があり、コンピュータが高速化し続けているため、すべてを無視することができます。問題は、C ++でプログラミングする理由ではなく、効率的な文字列連結についてスタックオーバーフローに質問を投稿する理由でもありません。
MrFox 2013年

7

.NET System.Stringsとは異なり、C ++のstd :: strings 可変であるため、単純な連結によって他のメソッドと同じくらい高速に構築できます。


2
特に、reserve()を使用して、開始前に結果のために十分な大きさのバッファーを作成する場合。
マークランサム

彼はoperator + =について話していると思います。それは縮退したケースですが、それはまた連結しています。P:私は、彼はC ++のいくつかの手がかりがある期待するように、ジェームズは、VC ++ MVPだった
ヨハネス・シャウブ- litb

1
彼がC ++について幅広い知識を持っていることは、疑いの余地はありません。質問について誤解があっただけです。呼び出されるたびに新しい文字列オブジェクトを返し、新しい文字バッファーを使用するoperator +の効率について質問しました。
ブライアンR.ボンディ

1
ええ。しかし、彼はoperator +が遅い場合を尋ねました、連結を行うための最良の方法は何ですか。そしてここでoperator + =が登場します。しかし、ジェームズの答えは少し短いことに同意します。それは私たち全員がoperator +を使用できるように聞こえます、そしてそれは最高の効率です:p
Johannes Schaub-litb

@ BrianR.Bondy operator+は新しい文字列を返す必要はありません。実装者は、そのオペランドが右辺値参照によって渡された場合、変更されたオペランドの1つを返すことができます。libstdc++ たとえば、これを行います。したがって、operator+テンポラリで呼び出すと、同じまたはほぼ同じパフォーマンスを実現できます。これは、ボトルネックを表すことを示すベンチマークがない限り、デフォルトにすることを支持する別の引数となる可能性があります。
underscore_d 2017年

5

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

しかし、おそらくそれを保守可能で理解可能な状態に保ち、実際に問題が発生しているかどうかを確認するためにプロファイリングする必要があるという意見には同意します。


2
stringstreamが遅い、groups.google.com
d /

1
@ArtemGr文字列ストリームは高速かもしれません。codeproject.com
Articles /

4

不完全なC ++、マシュー・ウィルソンが提示動的すべての部分を連結する前に、唯一の割り当てを有するために、最終的な文字列の長さを事前に計算する文字列の連結器を。また、式テンプレートを使用して静的連結子を実装することもできます。

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


Glib::ustring::compose()glibmmバインディングからGLibへのreserve()変換では、提供されたフォーマット文字列と可変引数に基づいて最終的な長さを推定およびs append()し、ループでそれぞれ(またはフォーマットされた置換)をsします。これはかなり一般的な作業方法だと思います。
underscore_d 2017年

4

std::string operator+新しい文字列を割り当て、2つのオペランド文字列を毎回コピーします。何度も繰り返すと、費用がかかります、O(n)。

std::string appendそしてoperator+=一方で、50%の文字列が成長する必要があるたびに容量をバンプ。これにより、メモリ割り当てとコピー操作の数が大幅に削減されます。O(log n)。


なぜこれが反対投票されたのか、私にはよくわかりません。基準では50%の数値は必須ではありませんが、IIRCまたはその100%は、実際の成長の一般的な尺度です。この回答の他のすべては異議のないようです。
underscore_d

数か月後、C ++ 11がデビューしてからかなり後に記述されているため、それほど正確ではないと思いますoperator+。一方または両方の引数が右辺値参照によって渡されるオーバーロードは、既存のバッファに連結することにより、新しい文字列をすべて割り当てることを回避できます。オペランドの1つ(容量が不十分な場合は再割り当てが必要になる場合があります)。
underscore_d 2017年

2

小さな文字列の場合は問題ではありません。大きな文字列がある場合は、ベクターまたは他のコレクションにパーツとして格納するのがよいでしょう。そして、1つの大きな文字列ではなく、そのようなデータのセットを処理するようにアルゴリズムを追加します。

複雑な連結にはstd :: ostringstreamを使用します。


2

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

大きな文字列をGUIに出力する場合は、出力先が文字列を大きな文字列としてよりも細かく処理できる可能性があります(たとえば、テキストエディターでテキストを連結する-通常、行は別々に保持されます)構造)。

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

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


2

結果の文字列にスペースを事前に割り当てる(予約する)と、おそらく最高のパフォーマンスになります。

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

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

トリックは、最初に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  |  

1
OPは、効率的に連結する方法に関心がありますstd::string。彼らは代替の文字列クラスを求めていません。
underscore_d 2017年

0

各アイテムのメモリ予約でこれを試すことができます:

namespace {
template<class C>
constexpr auto size(const C& c) -> decltype(c.size()) {
  return static_cast<std::size_t>(c.size());
}

constexpr std::size_t size(const char* string) {
  std::size_t size = 0;
  while (*(string + size) != '\0') {
    ++size;
  }
  return size;
}

template<class T, std::size_t N>
constexpr std::size_t size(const T (&)[N]) noexcept {
  return N;
}
}

template<typename... Args>
std::string concatStrings(Args&&... args) {
  auto s = (size(args) + ...);
  std::string result;
  result.reserve(s);
  return (result.append(std::forward<Args>(args)), ...);
}
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.