在C#中迭代字符串中单个字符的最快方法是什么?

Jos*_*nig 55 c# string

标题是问题.以下是我尝试通过研究回答这个问题.但我不相信我不知情的研究,所以我仍然提出问题(在C#中用字符串中的单个字符迭代的最快方法是什么?).

偶尔我想逐个循环遍历字符串的字符,例如在解析嵌套标记时 - 这是正则表达式无法完成的.我想知道最快的方法是迭代字符串中的各个字符,特别是非常大的字符串.

我自己做了一堆测试,结果如下.然而,有许多读者对.NET CLR和C#编译器有更深入的了解,所以我不知道我是否遗漏了一些明显的东西,或者我是否在测试代码中犯了错误.所以我征集你的集体回应.如果有人深入了解字符串索引器的实际工作方式,那将非常有帮助.(这是C#语言功能在幕后编译成其他东西吗?还是内置于CLR中的东西?).

使用流的第一种方法直接取自线程中接受的答案:如何从字符串生成流?

测试

longString是一个9910万字符的字符串,由89个纯文本版本的C#语言规范组成.显示的结果是20次迭代.如果存在'启动'时间(例如方法#3中隐式创建的数组的第一次迭代),我会单独测试它,例如在第一次迭代后断开循环.

结果

从我的测试中,使用ToCharArray()方法在char数组中缓存字符串是迭代整个字符串的最快速度.ToCharArray()方法是一项前期费用,对单个字符的后续访问速度略快于内置索引访问器.

                                           milliseconds
                                ---------------------------------
 Method                         Startup  Iteration  Total  StdDev
------------------------------  -------  ---------  -----  ------
 1 index accessor                     0        602    602       3
 2 explicit convert ToCharArray     165        410    582       3
 3 foreach (c in string.ToCharArray)168        455    623       3
 4 StringReader                       0       1150   1150      25
 5 StreamWriter => Stream           405       1940   2345      20
 6 GetBytes() => StreamReader       385       2065   2450      35
 7 GetBytes() => BinaryReader       385       5465   5850      80
 8 foreach (c in string)              0        960    960       4
Run Code Online (Sandbox Code Playgroud)

更新: Per @ Eric的评论,这里是100个迭代的结果,比一个更正常的1.1 M字符串(C#规范的一个副本).Indexer和char数组仍然是最快的,其次是foreach(字符串中的char),然后是stream方法.

                                           milliseconds
                                ---------------------------------
 Method                         Startup  Iteration  Total  StdDev
------------------------------  -------  ---------  -----  ------
 1 index accessor                     0        6.6    6.6    0.11
 2 explicit convert ToCharArray     2.4        5.0    7.4    0.30
 3 for(c in string.ToCharArray)     2.4        4.7    7.1    0.33
 4 StringReader                       0       14.0   14.0    1.21
 5 StreamWriter => Stream           5.3       21.8   27.1    0.46
 6 GetBytes() => StreamReader       4.4       23.6   28.0    0.65
 7 GetBytes() => BinaryReader       5.0       61.8   66.8    0.79
 8 foreach (c in string)              0       10.3   10.3    0.11     
Run Code Online (Sandbox Code Playgroud)

使用的代码(单独测试;为简洁起见,一起显示)

//1 index accessor
int strLength = longString.Length;
for (int i = 0; i < strLength; i++) { c = longString[i]; }

//2 explicit convert ToCharArray
int strLength = longString.Length;
char[] charArray = longString.ToCharArray();
for (int i = 0; i < strLength; i++) { c = charArray[i]; }

//3 for(c in string.ToCharArray)
foreach (char c in longString.ToCharArray()) { } 

//4 use StringReader
int strLength = longString.Length;
StringReader sr = new StringReader(longString);
for (int i = 0; i < strLength; i++) { c = Convert.ToChar(sr.Read()); }

//5 StreamWriter => StreamReader 
int strLength = longString.Length;
MemoryStream stream = new MemoryStream();
StreamWriter writer = new StreamWriter(stream);
writer.Write(longString);
writer.Flush();
stream.Position = 0;
StreamReader str = new StreamReader(stream);
while (stream.Position < strLength) { c = Convert.ToChar(str.Read()); } 

//6 GetBytes() => StreamReader
int strLength = longString.Length;
MemoryStream stream = new MemoryStream(Encoding.Unicode.GetBytes(longString));
StreamReader str = new StreamReader(stream);
while (stream.Position < strLength) { c = Convert.ToChar(str.Read()); }

//7 GetBytes() => BinaryReader 
int strLength = longString.Length;
MemoryStream stream = new MemoryStream(Encoding.Unicode.GetBytes(longString));
BinaryReader br = new BinaryReader(stream, Encoding.Unicode);
while (stream.Position < strLength) { c = br.ReadChar(); }

//8 foreach (c in string)
foreach (char c in longString) { } 
Run Code Online (Sandbox Code Playgroud)

接受的答案:

我解释了@CodeInChaos和Ben的笔记如下:

fixed (char* pString = longString) {
    char* pChar = pString;
    for (int i = 0; i < strLength; i++) {
        c = *pChar ;
        pChar++;
    }
}
Run Code Online (Sandbox Code Playgroud)

在短字符串上执行100次迭代的时间为4.4 ms,st dev为0.1 ms.

Jon*_*eet 27

foreach什么理由不包括?

foreach (char c in text)
{
    ...
}
Run Code Online (Sandbox Code Playgroud)

顺便说一句,这真的会成为你的性能瓶颈吗?迭代本身占总运行时间的比例是多少?

  • @jmh_gr:所以如果*all*你需要做的是迭代字符,我会用它.这是表达您意图的最简单的代码.如果速度足够快,请不要对更复杂的代码进行微优化. (9认同)
  • 它包含在方法3中.至于性能:所有这些方法都非常快,可能无关紧要.但知道仍然很有趣,特别是当它揭示了运行时或语言如何工作时. (3认同)
  • @jmh_gr - 不,不是.方法3迭代调用`ToCharArray`的结果. (3认同)
  • @jmh_gr:但是,如果你开始对*非常相似的*情况应用相同的测量,你可以得到非常不同的结果.使用`BinaryReader`看起来像是开始时的方式 - 我总是从最简单,最易读和可测试的代码开始,测量**,并在必要时进一步优化. (3认同)
  • @ one.beat.consumer:不,假设这是不安全的 - 因为在我添加答案之前它*不在那里.它是由OP编辑的. (3认同)

Han*_*ant 9

这种人工测试非常危险.值得注意的是,您的// 2和// 3版本的代码实际上从未对字符串进行索引.抖动优化器只是抛弃了代码,因为根本没有使用c变量.您只是测量for()循环所需的时间.除非您查看生成的机器代码,否则您无法真正看到这一点.

将其更改c += longString[i];为强制使用数组索引器.

当然这是胡说八道.仅配置真实代码.


Pie*_*kel 9

TL; DR:简单foreach是迭代字符串的最快方法.

对于那些回到这里的人来说:时代在变!

使用最新的.NET 64位JIT,不安全的版本实际上是最慢的.

以下是BenchmarkDotNet的基准实施.从这些,我得到以下结果:

          Method |      Mean |     Error |    StdDev |
---------------- |----------:|----------:|----------:|
        Indexing | 5.9712 us | 0.8738 us | 0.3116 us |
 IndexingOnArray | 8.2907 us | 0.8208 us | 0.2927 us |
  ForEachOnArray | 8.1919 us | 0.6505 us | 0.1690 us |
         ForEach | 5.6946 us | 0.0648 us | 0.0231 us |
          Unsafe | 7.2952 us | 1.1050 us | 0.3941 us |
Run Code Online (Sandbox Code Playgroud)

有趣的是那些不适用于数组副本的那个.这表明索引与foreach性能非常相似,差异为5%,foreach速度更快.使用unsafe实际上比使用a慢28%foreach.

在过去unsafe可能是最快的选择,但JIT一直在变得更快,更聪明.

作为参考,基准代码:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Configs;
using BenchmarkDotNet.Horology;
using BenchmarkDotNet.Jobs;
using BenchmarkDotNet.Running;

namespace StringIterationBenchmark
{
    public class StringIteration
    {
        public static void Main(string[] args)
        {
            var config = new ManualConfig();

            config.Add(DefaultConfig.Instance);

            config.Add(Job.Default
                .WithLaunchCount(1)
                .WithIterationTime(TimeInterval.FromMilliseconds(500))
                .WithWarmupCount(3)
                .WithTargetCount(6)
            );

            BenchmarkRunner.Run<StringIteration>(config);
        }

        private readonly string _longString = BuildLongString();

        private static string BuildLongString()
        {
            var sb = new StringBuilder();
            var random = new Random();

            while (sb.Length < 10000)
            {
                char c = (char)random.Next(char.MaxValue);
                if (!Char.IsControl(c))
                    sb.Append(c);
            }

            return sb.ToString();
        }

        [Benchmark]
        public char Indexing()
        {
            char c = '\0';
            var longString = _longString;
            int strLength = longString.Length;

            for (int i = 0; i < strLength; i++)
            {
                c |= longString[i];
            }

            return c;
        }

        [Benchmark]
        public char IndexingOnArray()
        {
            char c = '\0';
            var longString = _longString;
            int strLength = longString.Length;
            char[] charArray = longString.ToCharArray();

            for (int i = 0; i < strLength; i++)
            {
                c |= charArray[i];
            }

            return c;
        }

        [Benchmark]
        public char ForEachOnArray()
        {
            char c = '\0';
            var longString = _longString;

            foreach (char item in longString.ToCharArray())
            {
                c |= item;
            }

            return c;
        }

        [Benchmark]
        public char ForEach()
        {
            char c = '\0';
            var longString = _longString;

            foreach (char item in longString)
            {
                c |= item;
            }

            return c;
        }

        [Benchmark]
        public unsafe char Unsafe()
        {
            char c = '\0';
            var longString = _longString;
            int strLength = longString.Length;

            fixed (char* p = longString)
            {
                var p1 = p;

                for (int i = 0; i < strLength; i++)
                {
                    c |= *p1;
                    p1++;
                }
            }

            return c;
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

代码与提供的代码有一些细微的变化.从原始字符串中检索的字符是|-ed,并返回变量,然后返回值.原因是我们实际上需要对结果做些什么.否则,如果我们只是迭代字符串,如:

//8 foreach (c in string)
foreach (char c in longString) { } 
Run Code Online (Sandbox Code Playgroud)

JIT可以自由删除它,因为它可能会推断出你实际上并没有观察到迭代的结果.通过|对数组中的字符进行返回,BenchmarkDotNet将确保JIT无法执行此优化.


Ben*_*igt 8

最快的答案是使用C++/CLI:如何:访问System :: String中的字符

此方法使用指针算法迭代字符串中的字符.没有副本,没有隐式范围检查,也没有每个元素的函数调用.

通过编写不安全的C#版本,很可能从C#获得(几乎,C++/CLI不需要固定)相同的性能PtrToStringChars.

就像是:

unsafe char* PtrToStringContent(string s, out GCHandle pin)
{
    pin = GCHandle.Alloc(s, GCHandleType.Pinned);
    return (char*)pin.AddrOfPinnedObject().Add(System.Runtime.CompilerServices.RuntimeHelpers.OffsetToStringData).ToPointer();
}
Run Code Online (Sandbox Code Playgroud)

记得要GCHandle.Free事后打电话.

CodeInChaos的评论指出C#为此提供了语法糖:

fixed(char* pch = s) { ... }
Run Code Online (Sandbox Code Playgroud)

  • 你有什么方法可以发布一个如何从C#调用PtrToStringChars的例子吗?我读了你链接的文章以及[如何:将System :: String转换为wchar_t*或char*](http://msdn.microsoft.com/en-us/library/d1ae6tz5(v = VS.100) ).aspx),我得到了一般的想法,但我缺乏C++技能知道如何从C#调用它. (2认同)