私がCでプログラミングしていた頃のことを思い出します。2つの文字列が結合されると、OSは結合された文字列にメモリを割り当てる必要があり、プログラムはすべての文字列テキストをメモリの新しい領域にコピーし、古いメモリを手動でコピーする必要があります。解放されます。したがって、リストに参加する場合のように、これが複数回行われる場合、OSは次の連結後に解放するために、常により多くのメモリを割り当てる必要があります。 Cでこれを行うより良い方法は、結合された文字列の合計サイズを決定し、結合された文字列のリスト全体に必要なメモリを割り当てることです。
現在、現代のプログラミング言語(C#など)では、コレクションを反復処理し、すべての文字列を一度に1つの文字列参照に追加することで、コレクションのコンテンツが結合されるのをよく見ます。これは、現代のコンピューティングパワーを使用していても非効率ではありませんか?
非効率な理由は、少なくとも私がよく知っている言語(C、Java、C#)では正確ですが、大量の文字列連結を実行することは一般的に一般的ではないことに同意します。私が取り組んでいるC#コードでは、StringBuilder
、_String.Format
_などが大量に使用されています。
したがって、質問の答えを得るには、別の質問をする必要があります:文字列を連結することが本当に問題にならないのであれば、なぜStringBuilder
やStringBuffer
存在します?セミ初心者向けプログラミングの本やクラスにもこのようなクラスの使用が含まれているのはなぜですか?どうやら時期尚早の最適化のアドバイスがそれほど顕著になるのでしょうか?
ほとんどの文字列連結開発者が答えを純粋に経験に基づいているとすると、ほとんどの人はそれが違いを生むことはなく、そのようなツールの使用を避けて「より読みやすい」for (int i=0; i<1000; i++) { strA += strB; }
を優先するでしょう。 しかし、彼らはそれを測定したことはありません。
この質問に対する本当の答えは this SO answer にあります。これは、1つのインスタンスで50,000文字列を連結すると(アプリケーションによっては、よくあることですが)、たとえ小さなものであっても、1000xのパフォーマンスヒットが発生しました。
文字通りパフォーマンスがまったく意味がない場合は、必ず連結してください。しかし、代替(StringBuilder)を使用するのは難しいか、または読みづらいので、「時期尚早」を呼び出してはならない合理的なプログラミング手法であることに同意しません最適化」防御。
更新:
プラットフォームを理解し、悲しいことに普遍的ではないそのベストプラクティスに従ってください。 2つの異なる「現代言語」からの2つの例:
すべてのプラットフォームのすべてのニュアンスをすぐに知らないことは、まさに基本的な罪ではありませんが、このようなプラットフォームの重要な問題を無視することは、ほとんどJavaからC++に移行し、メモリの割り当て解除を気にしないことと同じです。
大体あなたが説明した理由のため、それは効率的ではありません。 C#とJava=の文字列は不変です。文字列の操作は、Cの場合とは異なり、元のインスタンスを変更するのではなく、個別のインスタンスを返します。複数の文字列を連結すると、各ステップで個別のインスタンスが作成されますこれらの未使用のインスタンスを割り当て、後でガベージコレクションを行うと、パフォーマンスが低下する可能性があります。今回のみ、ガベージコレクタによってメモリ管理が処理されます。
C#とJavaはどちらも、このタイプのタスク専用の可変文字列としてStringBuilderクラスを導入します。Cの同等の機能は、配列に結合するのではなく、連結された文字列のリンクリストを使用することです。C#は、文字列のコレクションを結合するための便利な文字列のJoinメソッドも提供します。
厳密に言うと、CPUサイクルの使用効率が低いため、正解です。しかし、開発者の時間、メンテナンスコストなどについてはどうでしょう。時間のコストを計算式に追加する場合、ほとんどの場合、最も簡単なことを行う方が効率的であり、必要に応じて、低速ビットをプロファイルして最適化します。
「プログラム最適化の最初のルール:実行しないでください。プログラム最適化の2番目のルール(専門家のみ!):まだ実行しないでください。」
実際のテストなしでは、パフォーマンスについて何かを言うことは非常に困難です。最近、JavaScriptでナイーブな文字列連結が通常、推奨される「リストを作成して結合する」ソリューション(テスト ここ 、t1とt4を比較する)よりも速いことを発見して、非常に驚きました。それがなぜ起こるのか私はまだ困惑しています。
パフォーマンス(特にメモリ使用量)について推論するときに尋ねる可能性があるいくつかの質問は次のとおりです。1)入力はどれくらい大きいですか。 2)私のコンパイラはどれくらい賢いですか? 3)ランタイムはどのようにメモリを管理しますか?これは完全ではありませんが、出発点です。
私の入力はどれくらい大きいですか?
複雑なソリューションでは、多くの場合、固定オーバーヘッドが発生します。実行される追加の操作の形式か、必要な追加のメモリが存在する可能性があります。これらのソリューションは大きなケースを処理するように設計されているため、コードをマイクロ最適化するよりも純利益がより重要であるため、実装者は通常、追加のコストを導入しても問題はありません。したがって、入力が十分に小さい場合、このオーバーヘッドを回避するためだけの場合でも、単純なソリューションの方が複雑なソリューションよりもパフォーマンスが優れている可能性があります。 (ただし、「十分に小さい」とは何かを判断することは難しい部分です)
私のコンパイラはどれくらい賢いですか?
多くのコンパイラーは、書き込まれるが読み取られない変数を「最適化」するのに十分なほどスマートです。同様に、優れたコンパイラはナイーブな文字列連結を(コア)ライブラリの使用に変換できる場合があり、それらの多くが読み取りなしで作成された場合、それらの操作の間に文字列に戻す必要はありません(たとえあなたのソースコードはまさにそれをしているようです)。私はそこにあるコンパイラがそれを行うかどうか、またはそれがどの程度行われるかを知ることができません(AFAIK Java少なくとも同じ式のいくつかの連結を一連のStringBuffer操作に置き換えます) 、しかしそれは可能性です。
ランタイムはどのようにメモリを管理しますか?
最近のCPUでは、ボトルネックは通常プロセッサではなくキャッシュです。コードが多くの「離れた」メモリアドレスに短時間でアクセスする場合、すべてのメモリをキャッシュレベル間で移動するのにかかる時間は、使用する命令のほとんどの最適化よりも優れています。最近生成された変数(たとえば、同じ関数スコープ内)は通常、連続したメモリアドレスにあるため、世代別ガベージコレクターを備えたランタイムでは、これは特に重要です。これらのランタイムは、メソッド呼び出し間でメモリを定期的に移動します。
文字列の連結に影響を与える可能性がある1つの方法(免責事項:これは大まかな推測ですが、確かなことを言うには十分な知識がありません)ナイーブなメモリが、それを使用する残りのコードの近くに割り当てられている場合(それが複数回割り当てて解放する場合)、ライブラリオブジェクトのメモリはそれから遠くに割り当てられます(そのため、コードの計算中に多くのコンテキストが変化し、ライブラリが消費し、コードがより多く計算するなど、多くのキャッシュミスが発生します)。もちろん、大きな入力OTOHの場合はとにかくキャッシュミスが発生するため、複数の割り当ての問題がより顕著になります。
そうは言っても、私はこの方法やその方法の使用を推奨しているわけではありません。今日のほとんどのシステムは複雑すぎて、対象に関する深い専門知識がなければ完全に理解できないため、テストとプロファイリングとベンチマークのみがパフォーマンスに関する理論分析の前に行う必要があります。
ジョエルはこの主題について 素晴らしい記事 を書いています。他の人が指摘したように、それは言語に大きく依存しています。文字列のCでの実装方法(ゼロで終了、長さフィールドなし)のため、標準のstrcatライブラリルーチンは非常に非効率的です。ジョエルは、はるかに効率的であるわずかな変更を伴う代替案を提示します。