Использование метода StringBuilder Remove более эффективно, чем создание нового StringBuilder в цикле?

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

10 ответов


несколько ответов мягко предложили мне слезть с моего Даффа и выяснить это самостоятельно, поэтому ниже приведены мои результаты. Я думаю, что эти настроения обычно идут против зерна этого сайта, но если вы хотите, чтобы что-то было сделано правильно, вы можете это сделать.... :)

Я изменил опцию #1, чтобы воспользоваться предложением @Ty для использования StringBuilder.Length = 0 вместо метода Remove. Это сделало код двух вариантов более похожим. Теперь два различия заключаются в том, является ли конструктор для StringBuilder находится в цикле или вне цикла, и опция #1 теперь использует метод the Length для очистки StringBuilder. Оба параметра были настроены на запуск массива outputStrings со 100 000 элементами, чтобы сборщик мусора выполнял некоторую работу.

несколько ответов предложили подсказки, чтобы посмотреть на различные счетчики PerfMon и такие и использовать результаты, чтобы выбрать вариант. Я провел некоторые исследования и в конечном итоге использовал встроенный Обозреватель производительности разработчика Visual Studio Team Systems издание, которое у меня на работе. Я нашел вторую запись в блоге многостраничной серии, которая объяснила, как ее настроить здесь. В принципе, вы проводите модульный тест, чтобы указать код, который вы хотите профилировать; пройдите мастер & некоторые конфигурации; и запустите профилирование модульного теста. Я включил метрики распределения и времени жизни объектов .NET. Результаты профилирования, где трудно отформатировать для этого ответа, поэтому я разместил их в конце. Если скопировать и вставить текст в Excel и помассируй их немного, они будут читабельными.

опция #1 является наиболее эффективной памятью, потому что она заставляет сборщик мусора делать немного меньше работы, и он выделяет половину памяти и экземпляров объекту StringBuilder, чем опция #2. Для ежедневного кодирования выбор варианта №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 "заставляет" 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'D.

обе строки из Варианта 1:

string output = formattedOutput.ToString();

и строка из Варианта 2:

ExistingOutputMethodThatOnlyTakesAString(
           formattedOutputInsideALoop.ToString());

создать неизменяемые объект со значением префикса + outputString + postfix. Эта строка одинакового размера независимо от того, как вы ее создаете. То, что вы действительно спрашиваете, является более эффективной памятью:

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

или

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

пропуск StringBuilder полностью будет более эффективной памятью, чем любой из вышеперечисленных.

Если вам действительно нужно знать, какой из двух более эффективен в вашем приложении (это, вероятно, будет зависеть от размера вашего списка, префикса и outputStrings) я бы рекомендовал профилировщик муравьев red-gatehttp://www.red-gate.com/products/ants_profiler/index.htm

Джейсон


ненавижу это говорить, но как насчет тестирования?


этот материал легко узнать самостоятельно. Запуск Системного Монитора.exe и добавьте счетчик для коллекций .NET Memory + Gen 0. Запустите тестовый код миллион раз. Вы увидите, что для опции №1 требуется половина количества коллекций, необходимых опции № 2.


мы говорил об этом раньше с Java, вот результаты [Release] версии C#:

Option #1 (10000000 iterations): 11264ms
Option #2 (10000000 iterations): 12779ms

Update: в моем ненаучном анализе, позволяющем выполнять два метода при мониторинге всех счетчиков производительности памяти в perfmon, не привело к какой-либо заметной разнице с любым методом (кроме того, что некоторые счетчики всплеск только во время выполнения любого теста).

и вот что я тест:

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 и 2 примерно одинаковы при запуске в одной итерации.


Я бы сказал, вариант №2, Если определенно более простой. С точки зрения производительности, звучит как то, что вам просто нужно проверить и увидеть. Я бы предположил, что это не имеет достаточного значения, чтобы выбрать менее простой вариант.


Я думаю, что Вариант 1 будет чуть больше эффективно, как новый объект не создается каждый раз. Сказав это, GC делает довольно хорошую работу по очистке ресурсов, как в Варианте 2.

Мне кажется, вы можете попасть в ловушку преждевременной оптимизации (корень всего зла -- кнут). Ваш IO займет гораздо больше ресурсов, чем строковый конструктор.

Я склонен идти с более ясным / более чистым вариантом, в этом случае вариант 2.

Роб


  1. измерения
  2. предварительно выделите как можно ближе к тому, сколько памяти вам понадобится
  3. Если скорость-ваше предпочтение, то рассмотрите довольно прямой многопоточный фронт до середины, от середины до конца параллельный подход (разверните разделение труда по мере необходимости)
  4. измерить его снова

что важнее тебя?

  1. скорость

  2. ясность