【问题标题】:Is the string ctor the fastest way to convert an IEnumerable<char> to string字符串 ctor 是将 IEnumerable<char> 转换为字符串的最快方法吗
【发布时间】:2013-01-24 14:08:12
【问题描述】:

为 .Net Core 2.1 的发布而编辑

重复测试.Net Core 2.1的发布,我得到这样的结果

“Concat”的 1000000 次迭代耗时 842 毫秒。

“新字符串”的 1000000 次迭代耗时 1009 毫秒。

“sb”的 1000000 次迭代耗时 902 毫秒。

简而言之,如果您使用的是 .Net Core 2.1 或更高版本,Concat 为王。


我已编辑该问题以纳入 cmets 中提出的有效点。


我在沉思my answer to a previous question,我开始怀疑,这是不是,

return new string(charSequence.ToArray());

IEnumerable&lt;char&gt; 转换为string 的最佳方式。我做了一些搜索,发现这个问题已经问过here。该答案断言,

string.Concat(charSequence)

是更好的选择。在回答了这个问题之后,还建议使用StringBuilder 枚举方法,

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

return sb.ToString();

虽然这可能有点笨拙,但为了完整起见,我将其包含在内。我决定我应该做一个小测试,使用的代码在底部。

当在发布模式下构建,经过优化,并在没有附加调试器的情况下从命令行运行时,我会得到这样的结果。

“Concat”的 1000000 次迭代耗时 1597 毫秒。

“新字符串”的 1000000 次迭代耗时 869 毫秒。

“sb”的 1000000 次迭代耗时 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 (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;
        }
    }
}

【问题讨论】:

  • 1.) 如果这是在调试模式下完成的,结果是不准确的,必须扔掉。 2.) 这听起来像premature optimization。如果它没有导致性能下降(我敢肯定它没有),那么测试性能可以解决什么问题?
  • 旁注:考虑测试真实的IEnumerable(即Enumerable.Repeat('d', 100))以避免构造函数/转换方法中的潜在捷径。
  • 要添加到@DaveZych 评论,您需要在发布模式下测试不附加调试器:在Visual Studio 中按Ctrl+F5。
  • @JimMischel,你说得很好,我已经在发布模式下进行了测试,没有附加调试器以获得我所说的结果。
  • @DaveZych, 1) 请参阅我之前的回复。 2) 考试的目的是回答问题。考虑到许多易于使用的选择,速度或性能是一个糟糕的差异化因素吗?

标签: c# performance


【解决方案1】:

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

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

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

  • 样式=字符串生成器 经过的时间是 00:01:05.9687330 分钟。
  • 样式=StringConcatFunction 经过的时间是 00:02:33.2672485 分钟。
  • 样式=字符串构造器 经过的时间是 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 ;
    }
  }
}

【讨论】:

  • StringBuilder 被实例化时,它需要分配内存。这个时间应该在字符串构建器方法中考虑。看起来您在字符串生成中抛出了随机的 mnumber 个昂贵的异常。测试可以/应该在相同的字符串上进行。虽然长度可能是一个重要因素,但我认为内容的随机性在这里并不重要。
  • 问题仍然存在:[the OP] 应该坚持使用 new string(...ToArray()) 还是,[he's] 是否缺少某些东西?跨度>
  • 这段代码有巨大的错误。对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。 (续)
  • (续)另外,使用共享的StringBuilder 是不公平的,因为instance = sb.ToString(); 会为除第一个呼叫之外的每个呼叫生成不正确的(累积的)string。为了解决这个问题,在每次调用 MakeString() 时应该调用 sb.Clear()(对于单线程代码)或创建自己的本地 StringBuilder(对于多线程代码),在这种情况下我会得到 StringBuilderSingleInstanceCleared=2:44.31 和 @987654346 @。最终,您的结论仍然是正确的,尽管StringBuilder 方法更像是 1.4x-2.9x,而不是 2.4x-3.7x,与其他方法一样快。
【解决方案2】:

值得注意的是,这些结果虽然从纯粹主义者的角度来看对于 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())
  • 我想关键是让人们了解选项。如果你真的有一个 IEnumerable 并且性能是一个很大的问题,那么我想我会努力使用一个扩展方法来使用一个字符串生成器,但是如果你知道你有一个数组,那么显然最好的选择是使用字符串构造函数。
猜你喜欢
  • 2011-12-27
  • 1970-01-01
  • 1970-01-01
  • 2014-06-08
  • 1970-01-01
  • 2015-08-27
  • 2023-03-03
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多