8

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);
}
4

10 に答える 10

7

回答のいくつかは、私がダフから降りて自分でそれを理解することを穏やかに示唆していたので、以下は私の結果です. その感情は一般的にこのサイトの性質に反すると思いますが、何かを正しく行いたい場合は、そうする方がよいでしょう.... :)

オプション #1 を変更して、Remove メソッドの代わりに StringBuilder.Length = 0 を使用するという @Ty の提案を利用しました。これにより、2 つのオプションのコードがより似たものになりました。2 つの違いは、StringBuilder のコンストラクターがループ内にあるかループ外にあるかということです。オプション 1 は、Length メソッドを使用して StringBuilder をクリアします。どちらのオプションも、100,000 個の要素を持つ outputStrings 配列に対して実行するように設定されており、ガベージ コレクターが何らかの作業を行うようになっています。

いくつかの回答は、さまざまな PerfMon カウンターなどを調べ、結果を使用してオプションを選択するためのヒントを提供しました。いくつかの調査を行った結果、職場で使用している Visual Studio Team Systems Developer エディションの組み込みの Performance Explorer を使用することになりました。セットアップ方法を説明しているマルチパート シリーズの 2 番目のブログ エントリを見つけまし。基本的に、プロファイリングするコードを指すように単体テストを接続します。ウィザードといくつかの構成を実行します。単体テストのプロファイリングを起動します。.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
于 2008-11-07T21:48:21.627 に答える
6

オプション 2 は、実際にはオプション 1 よりも優れているはずです (私は信じています)。呼び出しの行為によりRemove、StringBuilder は既に返された文字列のコピーを取得するように「強制」されます。文字列は実際には StringBuilder 内で可変であり、StringBuilder は必要でない限りコピーを取得しません。オプション 1 では、基本的にアレイをクリアする前にコピーします。オプション 2 では、コピーは必要ありません。

オプション 2 の唯一の欠点は、文字列が長くなってしまうと、追加中に複数のコピーが作成されることです。一方、オプション 1 ではバッファの元のサイズが維持されます。ただし、その場合は、余分なコピーを避けるために初期容量を指定してください。(サンプル コードでは、文字列はデフォルトの 16 文字よりも大きくなります。たとえば、32 の容量で初期化すると、必要な余分な文字列が削減されます。)

ただし、パフォーマンスは別として、オプション 2 の方がクリーンです。

于 2008-11-05T22:16:09.153 に答える
4

プロファイリング中に、ループに入ったときに StringBuilder の長さをゼロに設定することもできます。

formattedOutput.Length = 0;
于 2008-11-05T23:49:55.780 に答える
2

あなたはメモリだけに関心があるので、私はお勧めします:

foreach (string outputString in outputStrings)
    {    
        string output = "prefix " + outputString + " postfix";
        ExistingOutputMethodThatOnlyTakesAString(output)  
    }

output という名前の変数は、元の実装では同じサイズですが、他のオブジェクトは必要ありません。StringBuilder は文字列やその他のオブジェクトを内部で使用するため、GC が必要な多くのオブジェクトが作成されます。

オプション 1 の両方の行:

string output = formattedOutput.ToString();

そして、オプション 2 の行:

ExistingOutputMethodThatOnlyTakesAString(
           formattedOutputInsideALoop.ToString());

prefix + outputString + postfix の値を持つ不変オブジェクトを作成します。この文字列は、どのように作成しても同じサイズです。あなたが本当に求めているのは、どちらがよりメモリ効率が良いかということです:

    StringBuilder formattedOutput = new StringBuilder(); 
    // create new string builder

また

    formattedOutput.Remove(0, output.Length); 
    // reuse existing string builder

StringBuilder を完全にスキップすると、上記のいずれよりもメモリ効率が向上します。

アプリケーションでどちらがより効率的であるかを本当に知る必要がある場合 (これはおそらく、リスト、プレフィックス、および outputStrings のサイズによって異なります)、red-gate ANTS Profiler http://www.red-をお勧めします。 gate.com/products/ants_profiler/index.htm

ジェイソン

于 2008-11-05T23:41:56.593 に答える
1

これは自分で簡単に見つけることができます。Perfmon.exe を実行し、.NET メモリ + Gen 0 コレクションのカウンターを追加します。テスト コードを 100 万回実行します。オプション #1 では、オプション #2 が必要とするコレクションの数の半分が必要であることがわかります。

于 2008-11-05T22:27:52.140 に答える
1

言いたくないのですが、テストしてみませんか?

于 2008-11-05T22:19:28.613 に答える
1

これについては以前に Javaで説明しましたが、C# バージョンの [Release] 結果は次のとおりです。

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 は読みやすく維持しやすいです。この操作を何百万回も連続して実行しない限り、オプション 2 を使用します。1 回の反復で実行する場合、オプション 1 と 2 はほぼ同じであると思われるからです。

于 2008-11-05T22:34:23.390 に答える
0

間違いなくもっと簡単な場合は、オプション#2と言います。パフォーマンスに関しては、テストして確認する必要があるように思えます。簡単ではないオプションを選択しても、それほど違いはないと思います。

于 2008-11-05T22:13:46.473 に答える
0
  1. 測定する
  2. 必要と思われるメモリ量にできるだけ近いメモリを事前に割り当てます
  3. 速度が好みの場合は、かなり単純なマルチスレッド フロントからミドル、ミドルからエンドへの並行アプローチを検討してください (必要に応じて分業を拡大します)。
  4. もう一度測る

あなたにとってより重要なことは何ですか?

  1. メモリー

  2. 速度

  3. 明瞭さ

于 2008-11-06T15:55:16.503 に答える
0

オプション1は、新しいオブジェクトが毎回作成されるわけではないため、メモリ効率がわずかに向上すると思います。そうは言っても、GC はオプション 2 のようにリソースをきれいにクリーンアップします。

時期尚早の最適化 (すべての悪の根源 --Knuth )の罠に陥っている可能性があると思います。IO は文字列ビルダーよりもはるかに多くのリソースを必要とします。

私はよりクリアでクリーンなオプションを使用する傾向があり、この場合はオプション 2 です。

ロブ

于 2008-11-05T22:31:32.867 に答える