什么是最快速的方式来遍历C#中的string中的个别字符?

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

偶尔我想循环查看一个string的字符,比如parsing嵌套的标记时 – 这是用正则expression式无法完成的 。 我想知道最快的方法是遍历string中的单个字符,特别是非常大的string。

我自己做了一堆testing,结果如下。 然而,有很多读者对.NET CLR和C#编译器有深入的了解,所以我不知道是否遗漏了一些明显的东西,或者我在testing代码中犯了一个错误。 所以我恳求你的集体回应。 如果任何人有洞察到如何string索引实际工作,这将是非常有益的。 (这是一种C#语言特性编译成幕后的东西吗?还是内置到CLR中的东西?)。

第一个使用stream的方法直接从线程接受的答案中获取: 如何从string中生成stream?

testing

longString是一个9910万字符的string,由89个纯文本版本的C#语言规范组成。 显示的结果是20次迭代。 如果存在“启动”时间(例如方法#3中隐式创build的数组的第一次迭代),那么我会分别进行testing,例如在第一次迭代之后从循环中断开。

结果

在我的testing中,使用ToCharArray()方法将stringcaching在char数组中是迭代整个string的最快速度。 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 

更新: Per @ Eric的评论,这里是一个更正常的1.1 Mstring(C#规范的一个副本)100次迭代的结果。 索引器和字符数组仍然是最快的,其次是foreach(string中的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 

代码使用(单独testing;为简洁起见一并显示)

 //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和本的笔记如下:

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

对短string进行100次迭代的执行时间为4.4 ms,开发时间<0.1 ms。

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

这种方法使用指针运算来遍历string中的字符。 没有副本,没有隐式范围检查,也没有每个元素的函数调用。

通过编写一个不安全的C#版本的PtrToStringChars ,很有可能得到(几乎C ++ / CLI不需要固定)与C#相同的性能。

就像是:

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

记得打电话给GCHandle.Free后。

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

 fixed(char* pch = s) { ... } 

任何不包括foreach原因?

 foreach (char c in text) { ... } 

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

这些人工testing非常危险。 值得注意的是,你的代码// 2和// 3不会真正索引string。 抖动优化器只是抛弃了代码,因为cvariables根本没有被使用。 你只是测量for()循环需要多长时间。 除非您查看生成的机器代码,否则无法真正看到这一点。

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

当然这是无稽之谈。 只configuration真实的代码。

TL; DR:简单的foreach是迭代string的最快方法。

对于回头的人来说:时代变了!

使用最新的.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 | 

有趣的是不能在arrays副本上工作的。 这表明索引和foreach在性能上是非常相似的,有5%的差别, foreach速度更快 。 使用unsafe实际上比使用foreach慢了28%。

过去unsafe可能是最快的select,但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; } } } 

代码有一些从提供的代码的小的变化。 从原始string中检索的字符是| 用variables返回,并返回值。 其原因是我们实际上需要对结果做一些事情。 否则,如果我们只是迭代string,如:

 //8 foreach (c in string) foreach (char c in longString) { } 

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

如果微观优化对你来说非常重要,那么试试这个。 (为了简单,我假定inputstring的长度是8的倍数)

 unsafe void LoopString() { fixed (char* p = longString) { char c1,c2,c3,c4; Int64 len = longString.Length; Int64* lptr = (Int64*)p; Int64 l; for (int i = 0; i < len; i+=8) { l = *lptr; c1 = (char)(l & 0xffff); c2 = (char)(l >> 16); c3 = (char)(l >> 32); c4 = (char)(l >> 48); lptr++; } } } 

只是在开玩笑,从来没有使用这个代码:)

如果速度真的很重要比快速

 for (int i = 0; i < text.Length; i++) { char ch = text[i]; ... }