Является ли использование метода StringBuilder Remove более эффективным, чем создание нового цикла StringBuilder в цикле? - PullRequest
8 голосов
/ 06 ноября 2008

В 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 ]

7 голосов
/ 08 ноября 2008

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

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

Пара ответов предлагает подсказки, чтобы посмотреть на различные счетчики PerfMon и тому подобное и использовать результаты, чтобы выбрать опцию. Я провел небольшое исследование и в конечном итоге использовал встроенный Performance Explorer выпуска Visual Studio Team Systems Developer, который у меня есть на работе. Я нашел вторую запись в серии из нескольких частей, в которой объясняется, как ее настроить здесь . По сути, вы подключаете модульный тест, чтобы указать на код, который вы хотите профилировать; пройти через мастера и некоторые конфигурации; и запустите профилирование модульного теста. Я включил распределение объектов .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
6 голосов
/ 06 ноября 2008

Вариант 2 должен (я считаю) на самом деле превзойти вариант 1. Акт вызова Remove "заставляет" StringBuilder взять копию строки, которую он уже возвратил. Строка на самом деле является изменяемой в StringBuilder, и StringBuilder не берет копию, если в этом нет необходимости. С опцией 1 он копирует перед очисткой массива, а с опцией 2 копирование не требуется.

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

Однако, помимо производительности, вариант 2 просто чище.

4 голосов
/ 06 ноября 2008

Во время профилирования вы также можете попробовать установить нулевую длину StringBuilder при входе в цикл.

formattedOutput.Length = 0;
2 голосов
/ 06 ноября 2008

Так как вас интересует только память, я бы предложил:

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 + постфикс. Эта строка имеет одинаковый размер независимо от того, как вы ее создали. То, что вы действительно спрашиваете, что является более эффективным для памяти:

    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 / продукция / ants_profiler / index.htm

Jason

1 голос
/ 06 ноября 2008

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

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

Обновление: в моем ненаучном анализе, позволившем выполнить два метода при мониторинге всех счетчиков производительности памяти в 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 примерно одинаковы при работе в одной итерации.

1 голос
/ 06 ноября 2008

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

1 голос
/ 06 ноября 2008

Ненавижу это говорить, но как насчет того, чтобы просто проверить это?

0 голосов
/ 06 ноября 2008
  1. Мера
  2. Предварительно выделите как можно ближе к объему памяти, который, по вашему мнению, вам понадобится
  3. Если вы предпочитаете скорость, рассмотрите параллельный подход с многопоточностью от середины к концу (от середины до конца) (1003 *)
  4. измерить снова

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

  1. память

  2. Скорость

  3. ясность

0 голосов
/ 06 ноября 2008

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

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

Я предпочитаю использовать опцию «Очистить / очистить», в данном случае вариант 2.

Rob

0 голосов
/ 06 ноября 2008

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

...