簡體   English   中英

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

[英]What is the fastest way to iterate through individual characters in a string in C#?

標題是問題。 以下是我嘗試通過研究回答這個問題。 但我不相信我不知情的研究,所以我仍然提出問題(在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

更新: 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     

使用的代碼(單獨測試;為簡潔起見,一起顯示)

//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的筆記如下:

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

在短字符串上執行100次迭代的時間為4.4 ms,st dev為0.1 ms。

有什么理由不包括foreach

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

順便說一句,這真的會成為你的性能瓶頸嗎? 迭代本身占總運行時間的比例是多少?

這種人工測試非常危險。 值得注意的是,您的// 2和// 3版本的代碼實際上從未對字符串進行索引。 抖動優化器只是拋棄了代碼,因為根本沒有使用c變量。 您只是測量for()循環所需的時間。 除非您查看生成的機器代碼,否則您無法真正看到這一點。

將其更改為c += longString[i]; 強制使用數組索引器。

當然這是胡說八道。 僅配置真實代碼。

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

此方法使用指針算法迭代字符串中的字符。 沒有副本,沒有隱式范圍檢查,也沒有每個元素的函數調用。

通過編寫不安全的C#版本的PtrToStringChars ,很可能從C#獲得(幾乎C ++ / CLI不需要固定)相同的性能。

就像是:

 
 
 
  
  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) { ... }

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 |

有趣的是那些不適用於數組副本的那個。 這表明索引和foreach在性能上非常相似,差異為5%, foreach更快 使用unsafe實際上比使用foreach慢28%。

在過去, 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;
        }
    }
}

代碼與提供的代碼有一些細微的變化。 從原始字符串中檢索的字符為| -ed返回變量,然后返回值。 原因是我們實際上需要對結果做些什么。 否則,如果我們只是迭代字符串,如:

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

JIT可以自由刪除它,因為它可能會推斷出你實際上並沒有觀察到迭代的結果。 通過| - 在數組中的字符並返回它,BenchmarkDotNet將確保JIT無法執行此優化。

如果微優化對您來說非常重要,那么試試這個。 (為簡單起見,我假設輸入字符串的長度為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是要快foreach

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

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM