Подтвердить что ты не робот

Я подрываю эффективность StringBuilder?

Я начал использовать StringBuilder вместо прямой конкатенации, но похоже, что у него отсутствует решающий метод. Поэтому я сам реализовал это как расширение:

public void Append(this StringBuilder stringBuilder, params string[] args)
{
    foreach (string arg in args)
        stringBuilder.Append(arg);
}

Это превращает следующий беспорядок:

StringBuilder sb = new StringBuilder();
...
sb.Append(SettingNode);
sb.Append(KeyAttribute);
sb.Append(setting.Name);

В это:

sb.Append(SettingNode, KeyAttribute, setting.Name);

Я мог бы использовать sb.AppendFormat("{0}{1}{2}",..., но это кажется еще менее предпочтительным и еще труднее читать. Является ли мое расширение хорошим методом или это как-то подрывает преимущества StringBuilder? Я не пытаюсь преждевременно оптимизировать что-либо, так как мой метод больше читабельности, чем скорость, но я также хотел бы знать, что я не стреляю себе в ногу.

4b9b3361

Ответ 1

Я не вижу проблемы с вашим расширением. Если это сработает для вас, все это хорошо.

Я сам предпочитаю:

sb.Append(SettingNode)
  .Append(KeyAttribute)
  .Append(setting.Name);

Ответ 2

На такие вопросы всегда можно ответить с помощью простого тестового примера.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Diagnostics;

namespace SBTest
{
    class Program
    {
        private const int ITERATIONS = 1000000;

        private static void Main(string[] args)
        {
            Test1();
            Test2();
            Test3();
        }

        private static void Test1()
        {
            var sw = Stopwatch.StartNew();
            var sb = new StringBuilder();

            for (var i = 0; i < ITERATIONS; i++)
            {
                sb.Append("TEST" + i.ToString("00000"),
                          "TEST" + (i + 1).ToString("00000"),
                          "TEST" + (i + 2).ToString("00000"));
            }

            sw.Stop();
            Console.WriteLine("Testing Append() extension method...");
            Console.WriteLine("--------------------------------------------");
            Console.WriteLine("Test 1 iterations: {0:n0}", ITERATIONS);
            Console.WriteLine("Test 1 milliseconds: {0:n0}", sw.ElapsedMilliseconds);
            Console.WriteLine("Test 1 output length: {0:n0}", sb.Length);
            Console.WriteLine("");
        }

        private static void Test2()
        {
            var sw = Stopwatch.StartNew();
            var sb = new StringBuilder();

            for (var i = 0; i < ITERATIONS; i++)
            {
                sb.Append("TEST" + i.ToString("00000"));
                sb.Append("TEST" + (i+1).ToString("00000"));
                sb.Append("TEST" + (i+2).ToString("00000"));
            }

            sw.Stop();    
            Console.WriteLine("Testing multiple calls to Append() built-in method...");
            Console.WriteLine("--------------------------------------------");
            Console.WriteLine("Test 2 iterations: {0:n0}", ITERATIONS);
            Console.WriteLine("Test 2 milliseconds: {0:n0}", sw.ElapsedMilliseconds);
            Console.WriteLine("Test 2 output length: {0:n0}", sb.Length);
            Console.WriteLine("");
        }

        private static void Test3()
        {
            var sw = Stopwatch.StartNew();
            var sb = new StringBuilder();

            for (var i = 0; i < ITERATIONS; i++)
            {
                sb.AppendFormat("{0}{1}{2}",
                    "TEST" + i.ToString("00000"),
                    "TEST" + (i + 1).ToString("00000"),
                    "TEST" + (i + 2).ToString("00000"));
            }

            sw.Stop();
            Console.WriteLine("Testing AppendFormat() built-in method...");
            Console.WriteLine("--------------------------------------------");            
            Console.WriteLine("Test 3 iterations: {0:n0}", ITERATIONS);
            Console.WriteLine("Test 3 milliseconds: {0:n0}", sw.ElapsedMilliseconds);
            Console.WriteLine("Test 3 output length: {0:n0}", sb.Length);
            Console.WriteLine("");
        }
    }

    public static class SBExtentions
    {
        public static void Append(this StringBuilder sb, params string[] args)
        {
            foreach (var arg in args)
                sb.Append(arg);
        }
    }
}

На моем ПК вывод:

Testing Append() extension method...
--------------------------------------------
Test 1 iterations: 1,000,000
Test 1 milliseconds: 1,080
Test 1 output length: 29,700,006

Testing multiple calls to Append() built-in method...
--------------------------------------------
Test 2 iterations: 1,000,000
Test 2 milliseconds: 1,001
Test 2 output length: 29,700,006

Testing AppendFormat() built-in method...
--------------------------------------------
Test 3 iterations: 1,000,000
Test 3 milliseconds: 1,124
Test 3 output length: 29,700,006

Таким образом, ваш метод расширения только немного медленнее, чем метод Append(), и немного быстрее, чем метод AppendFormat(), но во всех трех случаях разница слишком тривиальна, чтобы беспокоиться. Таким образом, если ваш метод расширения улучшает читабельность вашего кода, используйте его!

Ответ 3

Это немного накладных, создающих дополнительный массив, но я сомневаюсь, что это много. Вы должны измерить

Если получается, что накладные расходы на создание массивов строк значительны, вы можете уменьшить его, имея несколько перегрузок - по одному для двух параметров, по одному на три, по одному на четыре и т.д.... так что только при достижении большего количества параметров (например, шесть или семь) потребуется создать массив. Перегрузки будут такими:

public void Append(this builder, string item1, string item2)
{
    builder.Append(item1);
    builder.Append(item2);
}

public void Append(this builder, string item1, string item2, string item3)
{
    builder.Append(item1);
    builder.Append(item2);
    builder.Append(item3);
}

public void Append(this builder, string item1, string item2,
                   string item3, string item4)
{
    builder.Append(item1);
    builder.Append(item2);
    builder.Append(item3);
    builder.Append(item4);
}

// etc

И затем одна окончательная перегрузка с использованием params, например

public void Append(this builder, string item1, string item2,
                   string item3, string item4, params string[] otherItems)
{
    builder.Append(item1);
    builder.Append(item2);
    builder.Append(item3);
    builder.Append(item4);
    foreach (string item in otherItems)
    {
        builder.Append(item);
    }
}

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

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

Ответ 4

Помимо немного накладных расходов, я лично не вижу никаких проблем с ним. Определенно более читабельны. Пока вы проходите разумное количество параметров, я не вижу проблемы.

Ответ 5

С точки зрения ясности ваше расширение в порядке.

Вероятно, было бы лучше просто использовать формат .append(x).append(y).append(z), если у вас никогда не было более 5 или 6 элементов.

StringBuilder сам по себе обеспечит вам прирост производительности, если вы обрабатываете много тысяч элементов. Кроме того, вы будете создавать массив каждый раз, когда вы вызываете метод.

Итак, если вы делаете это для ясности, это нормально. Если вы делаете это ради эффективности, то вы, вероятно, ошибетесь.

Ответ 6

Я бы не сказал, что вы подрываете его эффективность, но вы можете делать что-то неэффективное, когда доступен более эффективный метод. AppendFormat - это то, что я думаю, что вы хотите здесь. Если строка {0} {1} {2} используется постоянно, это слишком уродливо, я, как правило, помещаю свои строки форматирования в константы выше, поэтому внешний вид будет более или менее таким же, как ваш расширение.

sb.AppendFormat(SETTING_FORMAT, var1, var2, var3);

Ответ 7

Я не тестировал недавно, но в прошлом StringBuilder был на самом деле медленнее, чем простая конкатенация строк ( "this" + "that" ), пока вы не достигнете примерно 7 конкатенаций.

Если это конкатенация строк, которая не выполняется в цикле, вам может потребоваться рассмотреть, следует ли вообще использовать StringBuilder. (В цикле я начинаю беспокоиться о распределении с помощью простой конкатенации строк, поскольку строки неизменяемы.)

Ответ 8

Потенциально еще быстрее, потому что он выполняет не более одного шага перераспределения/копирования для многих приложений.

public void Append(this StringBuilder stringBuilder, params string[] args)
{
    int required = stringBuilder.Length;
    foreach (string arg in args)
        required += arg.Length;
    if (stringBuilder.Capacity < required)
        stringBuilder.Capacity = required;
    foreach (string arg in args)
        stringBuilder.Append(arg);
}

Ответ 9

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

Ответ 10

Крис

Вдохновленный этот ответ Джона Скита (второй ответ), я немного переписал ваш код. В принципе, я добавил метод TestRunner, который запускает переданную функцию и сообщает об истекшем времени, устраняя небольшой избыточный код. Не быть самодовольным, а скорее как упражнение по программированию для себя. Надеюсь, это будет полезно.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Diagnostics;

namespace SBTest
{
  class Program
  {
    private static void Main(string[] args)
    {
      // JIT everything
      AppendTest(1);
      AppendFormatTest(1);

      int iterations = 1000000;

      // Run Tests
      TestRunner(AppendTest, iterations);
      TestRunner(AppendFormatTest, iterations);

      Console.ReadLine();
    }

    private static void TestRunner(Func<int, long> action, int iterations)
    {
      GC.Collect();

      var sw = Stopwatch.StartNew();
      long length = action(iterations);
      sw.Stop();

      Console.WriteLine("--------------------- {0} -----------------------", action.Method.Name);
      Console.WriteLine("iterations: {0:n0}", iterations);
      Console.WriteLine("milliseconds: {0:n0}", sw.ElapsedMilliseconds);
      Console.WriteLine("output length: {0:n0}", length);
      Console.WriteLine("");
    }

    private static long AppendTest(int iterations)
    {
      var sb = new StringBuilder();

      for (var i = 0; i < iterations; i++)
      {
        sb.Append("TEST" + i.ToString("00000"),
                  "TEST" + (i + 1).ToString("00000"),
                  "TEST" + (i + 2).ToString("00000"));
      }

      return sb.Length;
    }

    private static long AppendFormatTest(int iterations)
    {
      var sb = new StringBuilder();

      for (var i = 0; i < iterations; i++)
      {
        sb.AppendFormat("{0}{1}{2}",
            "TEST" + i.ToString("00000"),
            "TEST" + (i + 1).ToString("00000"),
            "TEST" + (i + 2).ToString("00000"));
      }

      return sb.Length;
    }
  }

  public static class SBExtentions
  {
    public static void Append(this StringBuilder sb, params string[] args)
    {
      foreach (var arg in args)
        sb.Append(arg);
    }
  }
}

Здесь вывод:

--------------------- AppendTest -----------------------
iterations: 1,000,000
milliseconds: 1,274
output length: 29,700,006

--------------------- AppendFormatTest -----------------------
iterations: 1,000,000
milliseconds: 1,381
output length: 29,700,006