首页
学习
活动
专区
工具
TVP
发布
社区首页 >问答首页 >在C#中遍历字符串中的单个字符的最快方法是什么?

在C#中遍历字符串中的单个字符的最快方法是什么?
EN

Stack Overflow用户
提问于 2012-01-10 03:07:55
回答 5查看 39.1K关注 0票数 62

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

有时,我想逐个遍历字符串中的字符,比如在解析嵌套标记时--这是cannot be done with regular expressions。我想知道迭代字符串中的单个字符的最快方法是什么,特别是非常大的字符串。

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

使用流的第一个方法直接取自线程的接受答案:how to generate a stream from a string?

测试

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

结果

根据我的测试,使用ToCharArray()方法缓存字符数组中的字符串对于迭代整个字符串来说是最快的。ToCharArray()方法是一笔前期费用,随后对单个字符的访问比内置的索引访问器稍微快一些。

代码语言:javascript
复制
                                           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

更新:根据@Eric的评论,这是在一个更普通的1.1M字符字符串(一个C#规范的副本)上进行100次迭代的结果。索引器和char数组仍然是最快的,其次是foreach(字符串中的char),然后是流方法。

代码语言:javascript
复制
                                           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     

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

代码语言:javascript
复制
//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) { } 

接受的答案:

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

代码语言:javascript
复制
fixed (char* pString = longString) {
    char* pChar = pString;
    for (int i = 0; i < strLength; i++) {
        c = *pChar ;
        pChar++;
    }
}

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

EN

回答 5

Stack Overflow用户

发布于 2012-01-10 03:10:43

任何不包含foreach的理由

代码语言:javascript
复制
foreach (char c in text)
{
    ...
}

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

票数 31
EN

Stack Overflow用户

发布于 2012-01-10 03:44:25

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

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

当然,这是无稽之谈。仅分析真实代码。

票数 11
EN

Stack Overflow用户

发布于 2017-04-25 20:14:04

TL;DR: A simple foreach 是迭代字符串的最快方法。

对于回到这一点的人来说:时代变了!

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

下面是BenchmarkDotNet的基准实现。从中,我得到了以下结果:

代码语言:javascript
复制
          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 |

有趣的是那些在数组拷贝上不起作用的函数。这表明索引和foreach在性能上非常相似,只有5%的差异,foreach 是更快的。使用unsafe实际上比使用foreach慢28%。

在过去,unsafe可能是最快的选择,但它一直在变得更快、更智能。

作为参考,基准代码如下:

代码语言:javascript
复制
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;
        }
    }
}

该代码对所提供的代码进行了一些细微的更改。从原始字符串中检索到的字符是带有返回变量的|-ed,我们返回值。这样做的原因是我们实际上需要对结果做一些事情。否则,如果我们只是迭代字符串,如下所示:

代码语言:javascript
复制
//8 foreach (c in string)
foreach (char c in longString) { } 

JIT可以自由地删除它,因为它可以推断您实际上并没有观察到迭代的结果。通过|-ing数组中的字符并返回它,BenchmarkDotNet将确保JIT不能执行这种优化。

票数 11
EN
页面原文内容由Stack Overflow提供。腾讯云小微IT领域专用引擎提供翻译支持
原文链接:

https://stackoverflow.com/questions/8793762

复制
相关文章

相似问题

领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档