C #에서 문자열의 개별 문자를 반복하는 가장 빠른 방법은 무엇입니까?
제목은 질문입니다. 아래는 연구를 통해 대답하려는 나의 시도입니다. 그러나 나는 내 무식한 연구를 신뢰하지 않으므로 여전히 질문을 제기합니다 (C #에서 문자열의 개별 문자를 반복하는 가장 빠른 방법은 무엇입니까?).
때때로 중첩 된 토큰을 구문 분석 할 때와 같이 문자열의 문자를 하나씩 순환하고 싶습니다 . 정규 표현식으로는 수행 할 수 없습니다 . 문자열의 개별 문자, 특히 매우 큰 문자열을 반복하는 가장 빠른 방법이 무엇인지 궁금합니다.
나는 많은 테스트를 수행했으며 결과는 아래와 같습니다. 그러나 .NET CLR 및 C # 컴파일러에 대해 훨씬 더 깊이있는 지식을 가진 독자가 많이 있으므로 분명한 것이 누락되었는지 또는 테스트 코드에서 실수를했는지 알 수 없습니다. 그래서 나는 당신의 집단적인 반응을 간청합니다. 누군가 문자열 인덱서가 실제로 어떻게 작동하는지에 대한 통찰력이 있다면 매우 도움이 될 것입니다. (C # 언어 기능이 뒤에서 다른 것으로 컴파일 된 것입니까? 아니면 CLR에 내장 된 기능입니까?).
스트림을 사용하는 첫 번째 방법은 스레드의 승인 된 답변에서 직접 가져 왔습니다 . 문자열에서 스트림을 생성하는 방법은 무엇입니까?
테스트
longString
C # 언어 사양의 일반 텍스트 버전 89 개로 구성된 99.1 백만 개의 문자열입니다. 표시된 결과는 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
업데이트 : @Eric의 의견에 따라보다 일반적인 1.1M char 문자열 (C # 사양의 복사본 하나)에 대해 100 번 반복 한 결과가 있습니다. 인덱서 및 char 배열은 여전히 가장 빠르며, foreach (문자열의 char), 스트림 메서드가 뒤 따릅니다.
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.4ms였으며, st dev 미만은 0.1ms입니다.
가장 빠른 대답은 C ++ / CLI를 사용하는 것입니다. 방법 : System :: String의 문자 액세스
이 접근 방식은 포인터 산술을 사용하여 문자열의 내부 문자를 반복합니다. 복사본, 암시 적 범위 검사 및 요소 별 함수 호출이 없습니다.
안전하지 않은 C # 버전의 .NET Framework를 작성하여 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();
}
GCHandle.Free
나중에
전화하는 것을 잊지 마십시오
.
CodeInChaos의 의견은 C #이 이에 대한 구문 설탕을 제공한다고 지적합니다.
fixed(char* pch = s) { ... }
포함하지 않을 이유가 foreach
있습니까?
foreach (char c in text)
{
...
}
그런데 이것이 정말로 성능 병목 현상이 될까요? 반복 자체에 걸리는 총 실행 시간의 비율은 얼마입니까?
이런 종류의 인공 테스트는 꽤 위험합니다. 주목할만한 것은 // 2 및 // 3 버전의 코드가 실제로 문자열을 인덱싱하지 않는다는 것입니다. 지터 최적화 프로그램은 c 변수가 전혀 사용되지 않기 때문에 코드를 버립니다. for () 루프에 걸리는 시간을 측정하고 있습니다. 생성 된 기계어 코드를 보지 않으면 실제로 이것을 볼 수 없습니다.
c += longString[i];
배열 인덱서를 강제로 사용 하려면 로 변경하십시오 .
물론 말도 안됩니다. 실제 코드 만 프로파일 링하십시오 .
요약 : 간단한 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 = '\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) { }
the JIT is free to remove this because it could infer that you're not actually observing the results of the iteration. By |
-ing the characters in the array and returning this, BenchmarkDotNet will make sure that the JIT can't perform this optimization.
If micro optimization is very important for you, then try this. (I assumed input string's length to be multiple of 8 for simplicity)
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++;
}
}
}
Just kidding, never use this code :)
If speed really matters for
is faster than foreach
for (int i = 0; i < text.Length; i++) {
char ch = text[i];
...
}
ReferenceURL : https://stackoverflow.com/questions/8793762/what-is-the-fastest-way-to-iterate-through-individual-characters-in-a-string-in
'programing' 카테고리의 다른 글
ES6 수업은 왜 올리지 않나요? (0) | 2021.01.15 |
---|---|
PDF 문서에서 텍스트를 추출하는 방법은 무엇입니까? (0) | 2021.01.15 |
C (99)와 C ++ (11)의 비 호환 차이점은 무엇입니까? (0) | 2021.01.15 |
Callable과 유사한 인터페이스가 있지만 인수가 있습니까? (0) | 2021.01.15 |
JavaFX 2.1 : 툴킷이 초기화되지 않았습니다. (0) | 2021.01.15 |