Является ли строка ctor самым быстрым способом преобразования IEnumerable <char> в строку

У меня отредактирован вопрос о включении действительных точек, поднятых в комментариях.


Я размышлял над моим ответом на предыдущий вопрос, и я начал задаваться вопросом, так ли это,

return new string(charSequence.ToArray());

Лучший способ конвертировать IEnumerable<char> в string. Я сделал небольшой поиск и нашел этот вопрос уже заданный здесь. Этот ответ утверждает, что

string.Concat(charSequence)

- лучший выбор. После ответа на этот вопрос был предложен также метод подсчета StringBuilder,

var sb = new StringBuilder();
foreach (var c in chars)
{
    sb.Append(c);
}

return sb.ToString();

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

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

1000000 итераций "Конкат" заняло 1597 мс.

1000000 итераций "новой строки" заняло 869 мс.

1000000 итераций "sb" заняло 748 мс.

К моему расчёту, new string(...ToArray()) приближается к методу string.Concat в два раза быстрее. StringBuilder работает немного быстрее, но неудобно использовать, но может быть расширением.

Должен ли я придерживаться new string(...ToArray()) или, есть ли что-то, что мне не хватает?

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

class Program
{
    private static void Main()
    {
        const int iterations = 1000000;
        const string testData = "Some reasonably small test data";

        TestFunc(
            chars => new string(chars.ToArray()),
            TrueEnumerable(testData),
            10,
            "new String");

        TestFunc(
            string.Concat,
            TrueEnumerable(testData),
            10,
            "Concat");

        TestFunc(
            chars =>
                {
                    var sb = new StringBuilder();
                    foreach (ver c in chars)
                    {
                        sb.Append(c);
                    }

                    return sb.ToString();
                },
            TrueEnumerable(testData),
            10,
            "sb");

        Console.WriteLine("----------------------------------------");

        TestFunc(
            string.Concat,
            TrueEnumerable(testData),
            iterations,
            "Concat");

        TestFunc(
            chars => new string(chars.ToArray()),
            TrueEnumerable(testData),
            iterations,
            "new String");

        TestFunc(
            chars =>
                {
                    var sb = new StringBuilder();
                    foreach (ver c in chars)
                    {
                        sb.Append(c);
                    }

                    return sb.ToString();
                },
            TrueEnumerable(testData),
            iterations,
            "sb");

        Console.ReadKey();
    }

    private static TResult TestFunc<TData, TResult>(
            Func<TData, TResult> func,
            TData testData,
            int iterations)
    {
        var dummyResult = default(TResult);

        var stopwatch = Stopwatch.StartNew();
        for (var i = 0; i < iterations; i++)
        {
            dummyResult = func(testData);
        }

        stopwatch.Stop();
        Console.WriteLine(
            "{0} iterations of \"{1}\" took {2}ms.",
            iterations,
            func.Method,
            stopwatch.ElapsedMilliseconds);
    }
}

private static IEnumerable<T> TrueEnumerable<T>(IEnumerable<T> sequence)
{
    foreach (var t in sequence)
    {
        yield return t;
    }
}

Ответ 1

Стоит отметить, что эти результаты, хотя и верны для случая IEnumerable с точки зрения пуристов, не всегда таковы. Например, если бы у вас был массив char, даже если вы его передали как IEnumerable, быстрее вызвать конструктор строк.

Результаты:

Sending String as IEnumerable<char> 
10000 iterations of "new string" took 157ms. 
10000 iterations of "sb inline" took 150ms. 
10000 iterations of "string.Concat" took 237ms.
======================================== 
Sending char[] as IEnumerable<char> 
10000 iterations of "new string" took 10ms.
10000 iterations of "sb inline" took 168ms.
10000 iterations of "string.Concat" took 273ms.

Код:

static void Main(string[] args)
{
    TestCreation(10000, 1000);
    Console.ReadLine();
}

private static void TestCreation(int iterations, int length)
{
    char[] chars = GetChars(length).ToArray();
    string str = new string(chars);
    Console.WriteLine("Sending String as IEnumerable<char>");
    TestCreateMethod(str, iterations);
    Console.WriteLine("===========================================================");
    Console.WriteLine("Sending char[] as IEnumerable<char>");
    TestCreateMethod(chars, iterations);
    Console.ReadKey();
}

private static void TestCreateMethod(IEnumerable<char> testData, int iterations)
{
    TestFunc(chars => new string(chars.ToArray()), testData, iterations, "new string");
    TestFunc(chars =>
    {
        var sb = new StringBuilder();
        foreach (var c in chars)
        {
            sb.Append(c);
        }
        return sb.ToString();
    }, testData, iterations, "sb inline");
    TestFunc(string.Concat, testData, iterations, "string.Concat");
}

Ответ 2

Ну, я только что написал небольшой тест, попробовав 3 разных способа создания строки из IEnumerable:

  • с помощью StringBuilder и повторных вызовов его метода Append(char ch).
  • используя string.Concat<T>
  • с помощью конструктора String.

10000 итераций генерации последовательности случайных 1000 символов и построения строки из нее, я вижу следующие тайминги в сборке релизов:

  • Style = StringBuilder прошедшее время 00: 01: 05.9687330 минут.
  • Style = StringConcatFunction прошедшее время 00: 02: 33.2672485 минут.
  • Style = StringConstructor прошедшее время 00: 04: 00.5559091 минут.

StringBuilder явный победитель. Тем не менее, я использую статический экземпляр StringBuilder (singleton). Не знаю, если это имеет значение.

Здесь исходный код:

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

namespace ConsoleApplication6
{
  class Program
  {

    static readonly RandomNumberGenerator Random = RandomNumberGenerator.Create() ;

    static readonly byte[] buffer = {0,0} ;

    static char RandomChar()
    {
      ushort codepoint ;
      do
      {
        Random.GetBytes(buffer) ;
        codepoint = BitConverter.ToChar(buffer,0) ;
        codepoint &= 0x007F ; // restrict to Unicode C0 ;
      } while ( codepoint < 0x0020 ) ;
      return (char) codepoint ;
    }

    static IEnumerable<char> GetRandomChars( int count )
    {
      if ( count < 0 ) throw new ArgumentOutOfRangeException("count") ;

      while ( count-- >= 0 )
      {
        yield return RandomChar() ;
      }
    }

    enum Style
    {
      StringBuilder = 1 ,
      StringConcatFunction = 2 ,
      StringConstructor = 3 ,
    }

    static readonly StringBuilder sb = new StringBuilder() ;
    static string MakeString( Style style )
    {
      IEnumerable<char> chars = GetRandomChars(1000) ;
      string instance ;
      switch ( style )
      {
      case Style.StringConcatFunction :
        instance = String.Concat<char>( chars ) ;
        break ;
      case Style.StringBuilder : 
        foreach ( char ch in chars )
        {
          sb.Append(ch) ;
        }
        instance = sb.ToString() ;
        break ;
      case Style.StringConstructor :
        instance = new String( chars.ToArray() ) ;
        break ;
      default :
        throw new InvalidOperationException() ;
      }
      return instance ;
    }

    static void Main( string[] args )
    {
      Stopwatch stopwatch = new Stopwatch() ;

      foreach ( Style style in Enum.GetValues(typeof(Style)) )
      {
        stopwatch.Reset() ;
        stopwatch.Start() ;
        for ( int i = 0 ; i < 10000 ; ++i )
        {
          MakeString( Style.StringBuilder ) ;
        }
        stopwatch.Stop() ;
        Console.WriteLine( "Style={0}, elapsed time is {1}" ,
          style ,
          stopwatch.Elapsed
          ) ;
      }
      return ;
    }
  }
}