Какой самый быстрый способ перебора отдельных символов в строке в C#?
название-это вопрос. Ниже моя попытка ответить на него через исследования. Но я не доверяю своим неинформированным исследованиям, поэтому я все еще задаю вопрос (Каков самый быстрый способ перебора отдельных символов в строке в C#?).
иногда я хочу циклически перебирать символы строки один за другим, например, при анализе вложенных токенов-что-то, что не может быть сделано с регулярными выражениями. Мне интересно, какой самый быстрый способ-это перебирать отдельные символы в строке, особенно очень большие строки.
Я сделал кучу испытаний себя и мои результаты ниже. Однако есть много читателей с гораздо более глубокими знаниями о компиляторе .NET CLR и C#, поэтому я не знаю, пропустил ли я что-то очевидное, или если я сделал ошибку в своем тестовом коде. Поэтому я прошу вашего коллективного ответа. Если у кого-то есть представление о том, как на самом деле работает индексатор строк, это было бы очень полезно. (Это функция языка C# скомпилировано во что-то еще за кулисами? Или что-то встроенное в CLR?).
первый метод с использованием потока был взят непосредственно из принятого ответа из потока: как создать поток из строки?
тесты
longString
- это 99,1 миллиона символьных строк, состоящих из 89 копий текстовой версии спецификации языка C#. Результаты показаны для 20 итераций. Где есть время "запуска" (например для первой итерации неявно созданного массива в методе #3) я проверил это отдельно, например, путем разрыва цикла после первой итерации.
результаты
из моих тестов кэширование строки в массиве символов с помощью метода ToCharArray () является самым быстрым для итерации по всей строке. Метод 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
обновление: в комментарии @Eric, вот результаты для 100 итераций по более нормальной строке 1.1 M char (одна копия спецификации C#). Индексатор и массивы символов по-прежнему являются самыми быстрыми, за ними следуют foreach(char in string), а затем потоковые методы.
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 и заметки Бена следующим образом:
fixed (char* pString = longString) {
char* pChar = pString;
for (int i = 0; i < strLength; i++) {
c = *pChar ;
pChar++;
}
}
выполнение для 100 итераций по короткой строке составило 4,4 МС, с
6 ответов:
самый быстрый ответ-использовать C++ / CLI:как получить доступ к символам в системе:: String
этот подход повторяет символы на месте в строке, используя арифметику указателя. Нет никаких копий, никаких неявных проверок диапазона и никаких вызовов функций для каждого элемента.
вероятно, можно получить (почти, C++/CLI не требует закрепления) ту же производительность от C#, написав небезопасную версию C#
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(); }
Не забудьте назвать
GCHandle.Free
потом.комментарий CodeInChaos указывает, что C# предоставляет синтаксический сахар для этого:
fixed(char* pch = s) { ... }
любая причина не включать
foreach
?foreach (char c in text) { ... }
это действительно будет вашим узким местом производительности, кстати? Какую долю от общего времени выполнения занимает сама итерация?
такие искусственные тесты довольно опасны. Примечательно, что ваши //2 и //3 версии кода никогда не индексируют строку. Оптимизатор дрожания просто выбрасывает код, так как переменная c вообще не используется. Вы просто измеряете, сколько времени занимает цикл for (). Вы не можете действительно увидеть это, если не посмотрите на сгенерированный машинный код.
изменить на
c += longString[i];
для принудительного использования индексатора массива.что, конечно, нонсенс. Только профиль реальные код.
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
на самом деле на 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 = ''; 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 = ''; 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 = ''; var longString = _longString; foreach (char item in longString.ToCharArray()) { c |= item; } return c; } [Benchmark] public char ForEach() { char c = ''; var longString = _longString; foreach (char item in longString) { c |= item; } return c; } [Benchmark] public unsafe char Unsafe() { char c = ''; 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 может удалить это, потому что он может сделать вывод, что вы на самом деле не наблюдаете результаты итерации. На
|
- ing символы в массиве и возвращая это, 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++; } } }
шучу, никогда не используйте этот код :)