StringBuilder Removeメソッドを使用すると、ループで新しいStringBuilderを作成するよりもメモリ効率が高くなりますか?
-
06-07-2019 - |
質問
よりメモリ効率の高いC#の場合:オプション#1またはオプション#2?
public void TestStringBuilder()
{
//potentially a collection with several hundred items:
string[] outputStrings = new string[] { "test1", "test2", "test3" };
//Option #1
StringBuilder formattedOutput = new StringBuilder();
foreach (string outputString in outputStrings)
{
formattedOutput.Append("prefix ");
formattedOutput.Append(outputString);
formattedOutput.Append(" postfix");
string output = formattedOutput.ToString();
ExistingOutputMethodThatOnlyTakesAString(output);
//Clear existing string to make ready for next iteration:
formattedOutput.Remove(0, output.Length);
}
//Option #2
foreach (string outputString in outputStrings)
{
StringBuilder formattedOutputInsideALoop = new StringBuilder();
formattedOutputInsideALoop.Append("prefix ");
formattedOutputInsideALoop.Append(outputString);
formattedOutputInsideALoop.Append(" postfix");
ExistingOutputMethodThatOnlyTakesAString(
formattedOutputInsideALoop.ToString());
}
}
private void ExistingOutputMethodThatOnlyTakesAString(string output)
{
//This method actually writes out to a file.
System.Console.WriteLine(output);
}
解決
いくつかの答えは、私がダフを降りて自分でそれを理解することを穏やかに示唆したので、以下は私の結果です。感情は一般にこのサイトの穀物に反すると思いますが、何かを正しくしたい場合は、そうすることもできます....:)
オプション#1を変更して、@ Tyの提案を利用して、Removeメソッドの代わりにStringBuilder.Length = 0を使用しました。これにより、2つのオプションのコードがより似たものになりました。 2つの違いは、StringBuilderのコンストラクターがループ内にあるかループ外にあるかであり、オプション#1では、Lengthメソッドを使用してStringBuilderをクリアするようになりました。両方のオプションは、ガベージコレクターに何らかの作業を行わせるために、100,000個の要素を持つoutputStrings配列で実行されるように設定されました。
いくつかの回答では、さまざまなPerfMonカウンターと&そのような結果を使用してオプションを選択します。いくつかの調査を行った結果、仕事中のVisual Studio Team Systems Developerエディションに組み込まれているPerformance Explorerを使用しました。 こちら。基本的に、ユニットテストを接続して、プロファイルするコードを指定します。ウィザードを実行します&いくつかの構成。単体テストプロファイリングを起動します。 .NETオブジェクトの割り当てを有効にしました&ライフタイムメトリック。プロファイリングの結果は、この回答の書式設定が難しいため、最後に配置しました。テキストをコピーしてExcelに貼り付けて少しマッサージすると、読みやすくなります。
オプション#1は、ガベージコレクターの作業を少し少なくし、オプション#2よりもメモリとインスタンスの半分をStringBuilderオブジェクトに割り当てるため、メモリ効率が最も高くなります。日常的なコーディングでは、オプション#2を選択することでまったく問題ありません。
あなたがまだ読んでいるなら、オプション#2がC / C ++開発者の経験のメモリリークディテクタを弾道的にするので、私はこの質問をしました。 StringBuilderインスタンスが再割り当てされる前に解放されないと、巨大なメモリリークが発生します。もちろん、C#の開発者はそのようなことを心配しません(彼らが飛び上がって噛み付くまで)。すべてに感謝!!
ClassName Instances TotalBytesAllocated Gen0_InstancesCollected Gen0BytesCollected Gen1InstancesCollected Gen1BytesCollected
=======Option #1
System.Text.StringBuilder 100,001 2,000,020 100,016 2,000,320 2 40
System.String 301,020 32,587,168 201,147 11,165,268 3 246
System.Char[] 200,000 8,977,780 200,022 8,979,678 2 90
System.String[] 1 400,016 26 1,512 0 0
System.Int32 100,000 1,200,000 100,061 1,200,732 2 24
System.Object[] 100,000 2,000,000 100,070 2,004,092 2 40
======Option #2
System.Text.StringBuilder 200,000 4,000,000 200,011 4,000,220 4 80
System.String 401,018 37,587,036 301,127 16,164,318 3 214
System.Char[] 200,000 9,377,780 200,024 9,379,768 0 0
System.String[] 1 400,016 20 1,208 0 0
System.Int32 100,000 1,200,000 100,051 1,200,612 1 12
System.Object[] 100,000 2,000,000 100,058 2,003,004 1 20
他のヒント
オプション2は、実際にはオプション1よりも優れているはずです( Remove
" forces"既に返された文字列のコピーを取得するStringBuilder文字列は実際にはStringBuilder内で変更可能であり、必要でない限りStringBuilderはコピーを取得しません。オプション1では、基本的に配列をクリアする前にコピーします-オプション2では、コピーは不要です。
オプション2の唯一の欠点は、文字列が長くなると、追加中に複数のコピーが作成されることです-オプション1はバッファの元のサイズを保持します。ただし、この場合、余分なコピーを避けるために初期容量を指定してください。 (サンプルコードでは、文字列はデフォルトの16文字よりも大きくなります。たとえば、32文字の容量で初期化すると、余分な文字列が減ります。)
ただし、パフォーマンスは別として、オプション2はよりクリーンです。
プロファイリング中に、ループに入るときにStringBuilderの長さをゼロに設定することもできます。
formattedOutput.Length = 0;
メモリのみに関心があるので、以下をお勧めします:
foreach (string outputString in outputStrings)
{
string output = "prefix " + outputString + " postfix";
ExistingOutputMethodThatOnlyTakesAString(output)
}
outputという名前の変数は、元の実装と同じサイズですが、他のオブジェクトは必要ありません。 StringBuilderは文字列と他のオブジェクトを内部で使用し、GCが必要な多くのオブジェクトが作成されます。
オプション1の両方の行:
string output = formattedOutput.ToString();
オプション2の行:
ExistingOutputMethodThatOnlyTakesAString(
formattedOutputInsideALoop.ToString());
は、プレフィックス+ outputString +ポストフィックスの値を持つ immutable オブジェクトを作成します。この文字列は、作成方法に関係なく同じサイズです。あなたが本当に求めているのは、どちらがよりメモリ効率が良いかです:
StringBuilder formattedOutput = new StringBuilder();
// create new string builder
または
formattedOutput.Remove(0, output.Length);
// reuse existing string builder
StringBuilderを完全にスキップすると、上記のいずれよりもメモリ効率が高くなります。
2つのうちどちらがアプリケーションでより効率的かを本当に知る必要がある場合(これはおそらく、リスト、プレフィックス、およびoutputStringsのサイズに応じて変化します)ANTSプロファイラー http://www.red-gate.com/products/ants_profiler/index.htm
ジェイソン
それを言うのは嫌ですが、それをテストするだけではどうですか?
これは、自分で簡単に見つけることができます。 Perfmon.exeを実行し、.NETメモリ+ Gen 0コレクションのカウンターを追加します。テストコードを100万回実行します。オプション#1にはオプション#2が必要とするコレクションの数の半分が必要であることがわかります。
Javaでこれについて以前に話しました、ここにありますC#バージョンの[リリース]結果:
Option #1 (10000000 iterations): 11264ms
Option #2 (10000000 iterations): 12779ms
更新:私の非科学的な分析では、perfmonのすべてのメモリパフォーマンスカウンターを監視しながら2つのメソッドを実行できるため、どちらのメソッドでも識別可能な違いはありませんでした実行)。
そして、これが私がテストに使用したものです:
class Program
{
const int __iterations = 10000000;
static void Main(string[] args)
{
TestStringBuilder();
Console.ReadLine();
}
public static void TestStringBuilder()
{
//potentially a collection with several hundred items:
var outputStrings = new [] { "test1", "test2", "test3" };
var stopWatch = new Stopwatch();
//Option #1
stopWatch.Start();
var formattedOutput = new StringBuilder();
for (var i = 0; i < __iterations; i++)
{
foreach (var outputString in outputStrings)
{
formattedOutput.Append("prefix ");
formattedOutput.Append(outputString);
formattedOutput.Append(" postfix");
var output = formattedOutput.ToString();
ExistingOutputMethodThatOnlyTakesAString(output);
//Clear existing string to make ready for next iteration:
formattedOutput.Remove(0, output.Length);
}
}
stopWatch.Stop();
Console.WriteLine("Option #1 ({1} iterations): {0}ms", stopWatch.ElapsedMilliseconds, __iterations);
Console.ReadLine();
stopWatch.Reset();
//Option #2
stopWatch.Start();
for (var i = 0; i < __iterations; i++)
{
foreach (var outputString in outputStrings)
{
StringBuilder formattedOutputInsideALoop = new StringBuilder();
formattedOutputInsideALoop.Append("prefix ");
formattedOutputInsideALoop.Append(outputString);
formattedOutputInsideALoop.Append(" postfix");
ExistingOutputMethodThatOnlyTakesAString(
formattedOutputInsideALoop.ToString());
}
}
stopWatch.Stop();
Console.WriteLine("Option #2 ({1} iterations): {0}ms", stopWatch.ElapsedMilliseconds, __iterations);
}
private static void ExistingOutputMethodThatOnlyTakesAString(string s)
{
// do nothing
}
}
このシナリオのオプション1はわずかに高速ですが、オプション2の方が読みやすく、保守も簡単です。この操作を何百万回も連続して実行しない限り、1回の繰り返しで実行する場合はオプション1と2はほぼ同じであると思われるため、オプション2に固執します。
間違いなくより簡単な場合、オプション#2を言うでしょう。パフォーマンスの面では、テストして確認するだけでいいように思えます。それほど単純ではないオプションを選択するのに十分な違いはないと思います。
新しいオブジェクトが毎回作成されるわけではないため、オプション1の方がメモリの方がわずかに効率的だと思います。そうは言っても、GCはオプション2のようにリソースをクリーンアップするかなり良い仕事をします。
あなたは時期尚早な最適化のtrapに陥っていると思う(すべての悪の根源- -クヌース)。 IOは、文字列ビルダーよりも多くのリソースを使用します。
私は、より明確な/よりきれいなオプション、この場合はオプション2を選択する傾向があります。
ロブ
- 測定
- 必要と思われるメモリ量にできるだけ近い場所に事前に割り当てます
- 速度を優先する場合は、フロントからミドル、ミドルからエンドへの並行アプローチをかなり単純に進めます(必要に応じて分業を拡大します)
- もう一度測定
あなたにとってより重要なことは何ですか?
-
メモリ
-
速度
-
明瞭性