字符串构造函数是否是将IEnumerable<char>转换为字符串的最快方法?

11

针对 .Net Core 2.1 版本进行编辑

重复测试 .Net Core 2.1 版本后,我得到了如下结果:

对 "Concat" 进行 1000000 次迭代,耗时 842ms。

对 "new String" 进行 1000000 次迭代,耗时 1009ms。

对 "sb" 进行 1000000 次迭代,耗时 902ms。

简而言之,如果您正在使用 .Net Core 2.1 或更高版本,则 Concat 是最佳选择。


我已经在问题中 编辑 了内容,以包含评论中提出的有效观点。


我正在思考 我之前回答的一个问题,并开始想知道,这是否是这样的情况:

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次“Concat”的迭代需要1597ms。 1000000次“new String”的迭代需要869ms。 1000000次“sb”的迭代需要748ms。
据我估计,`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 (var 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 (var 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,
            string stage)
    {
        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 \"{2}\" took {1}ms.",
            iterations,
            stopwatch.ElapsedMilliseconds,
            stage);

        return dummyResult;
    }

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

5
1.) 如果是在调试模式下完成的,结果可能不准确,必须丢弃。 2.) 这听起来像是过早优化。如果它没有导致性能损失(我确定它没有),测试性能解决什么问题? - Dave Zych
4
附注:考虑在真正的 IEnumerable 对象上进行测试(例如 Enumerable.Repeat('d', 100)),以避免构造函数/转换方法中潜在的快捷方式。 - Alexei Levenkov
1
补充@DaveZych的评论,您需要在发布模式下进行测试不附加调试器:在Visual Studio中按Ctrl + F5。 - Jim Mischel
1
@JimMischel,你说得很对,我已经在发布模式下进行了测试,没有连接调试器,以获取我所描述的结果。 - Jodrell
1
@DaveZych,1)请查看我之前的回复。2)测试的目的是回答这个问题。如果所有选择都很容易使用,那么速度或性能是否是一个不好的区分因素? - Jodrell
1
你有第四个选项:string.Join("", charSequence)。我希望它在性能方面与构造函数/字符串生成器接近。 - nawfal
2个回答

7
值得注意的是,从纯粹主义者的角度来看,这些结果对于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");
}

你的结果和我的结果很相似。除了 string.Concat 稍微更容易打一些,我看不出为什么我要用它来代替 new string(...ToArray()) - Jodrell
我想关键是让人们理解选项。如果您真的拥有IEnumerable并且性能非常重要,那么我认为我会费力地编写扩展方法来使用字符串构建器,但如果您知道您有一个数组,那么显然最好的选择是使用字符串构造函数。 - Baguazhang

1

我刚刚写了一个小测试,尝试用3种不同的方法从IEnumerable中创建字符串:

  1. 使用StringBuilder并重复调用其Append(char ch)方法。
  2. 使用string.Concat<T>
  3. 使用String构造函数。

在生成一个随机的1000个字符序列并从中构建字符串的10000次迭代中,我在发布版本中看到以下时间:

  • Style=StringBuilder 经过00:01:05.9687330分钟。
  • Style=StringConcatFunction 经过00:02:33.2672485分钟。
  • Style=StringConstructor 经过00:04:00.5559091分钟。

StringBuilder是明显的赢家。虽然我正在使用静态StringBuilder(单例)实例,但不知道这是否会有很大的区别。

以下是源代码:

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

2
当实例化StringBuilder时,需要分配内存。这个时间应该在字符串生成的过程中加以考虑。同时,看起来你在字符串生成过程中抛出了大量昂贵的异常。测试应该在同一个字符串上进行。虽然长度可能是一个重要因素,但我认为内容的随机性在这里并不相关。 - Jodrell
问题仍然存在:[OP] 应该坚持使用 new string(...ToArray()),还是有什么 [他] 没有注意到的东西? - default
这段代码有严重的漏洞。调用MakeString(Style.StringBuilder);应该是MakeString(style);,否则你只是将StringBuilder方法与自身进行比较!每个foreach迭代随着共享的StringBuilder变得越来越大而变慢;如果为每个style值创建一个新实例,则(在修复其他错误的情况下)测试会产生类似的时间。使用Visual Studio 2017 v15.7.5/.NET v4.7.1修复了MakeString()调用后,我得到了StringBuilder=0:59.41StringConcatFunction=6:13.75StringConstructor=7:56.22。(续) - Lance U. Matthews
此外,使用共享的 StringBuilder 是不公平的,因为 instance = sb.ToString(); 对于除第一次调用之外的每次调用都会产生一个不正确(累积)的 string。为了解决这个问题,在每次调用 MakeString() 时,应该调用 sb.Clear()(对于单线程代码)或创建自己的本地 StringBuilder(对于多线程代码),在这种情况下,我得到了 StringBuilderSingleInstanceCleared=2:44.31StringBuilderInstancePerCall=4:27.73。最终,你的结论仍然是正确的,尽管 StringBuilder 方法比其他方法快大约 1.4 倍至 2.9 倍,而不是 2.4 倍至 3.7 倍。 - Lance U. Matthews

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接