quitar - string c# ejemplos
¿Cuál es la forma más rápida de iterar a través de caracteres individuales en una cadena en C#? (6)
¿Alguna razón para no incluir foreach
?
foreach (char c in text)
{
...
}
Por cierto, ¿será este su cuello de botella de rendimiento? ¿Qué proporción del tiempo total de ejecución toma la iteración en sí?
El título es la pregunta. Debajo está mi intento de responderlo a través de la investigación. Pero no confío en mi investigación desinformada, por lo que sigo planteando la pregunta (¿Cuál es la forma más rápida de iterar a través de caracteres individuales en una cadena en C #?).
Ocasionalmente, quiero recorrer los caracteres de una cadena uno a uno, como cuando analizo tokens anidados, algo que no se puede hacer con expresiones regulares . Me pregunto cuál es la forma más rápida de iterar a través de los caracteres individuales en una cadena, particularmente cadenas muy grandes.
Hice muchas pruebas yo mismo y mis resultados están por debajo. Sin embargo, hay muchos lectores con un conocimiento mucho más profundo del compilador .NET CLR y C #, por lo que no sé si me falta algo obvio o si cometí un error en el código de mi prueba. Entonces solicito su respuesta colectiva. Si alguien tiene una idea de cómo funciona realmente el indexador de cadenas, sería muy útil. (¿Es una característica del lenguaje C # compilada en algo más detrás de escena? ¿O algo incorporado en el CLR?).
El primer método que utiliza una secuencia se tomó directamente de la respuesta aceptada del hilo: ¿cómo generar una secuencia a partir de una cadena?
Pruebas
longString
es una cadena de 99,1 millones de caracteres que consta de 89 copias de la versión de texto sin formato de la especificación de lenguaje C #. Los resultados mostrados son para 20 iteraciones. Donde hay un tiempo de ''inicio'' (como en el caso de la primera iteración de la matriz creada implícitamente en el método n. ° 3), lo probé por separado, por ejemplo, al interrumpir el ciclo después de la primera iteración.
Resultados
A partir de mis pruebas, el cache de la cadena en una matriz char usando el método ToCharArray () es el más rápido para iterar sobre toda la cadena. El método ToCharArray () es un gasto inicial, y el acceso posterior a caracteres individuales es ligeramente más rápido que el acceso de índice incorporado.
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
Actualización: Comentario de Per @ Eric, aquí hay resultados para 100 iteraciones sobre una cadena de caracteres de 1.1 M más normal (una copia de la especificación de C #). Los arreglos indexer y char son aún más rápidos, seguidos de foreach (char en string), seguidos de los métodos de transmisión.
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
Código utilizado (probado por separado, se muestran juntos para abreviar)
//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) { }
Respuesta aceptada:
Interpreté @CodeInChaos y las notas de Ben de la siguiente manera:
fixed (char* pString = longString) {
char* pChar = pString;
for (int i = 0; i < strLength; i++) {
c = *pChar ;
pChar++;
}
}
La ejecución de 100 iteraciones sobre la cadena corta fue de 4,4 ms, con <0,1 ms st dev.
Este tipo de pruebas artificiales son bastante peligrosas. Es notable que sus versiones // 2 y // 3 del código nunca indexan realmente la cadena. El optimizador de jitter simplemente descarta el código ya que la variable c no se usa en absoluto. Solo está midiendo cuánto dura el ciclo for (). Realmente no puede ver esto a menos que mire el código máquina generado.
c += longString[i];
a c += longString[i];
para forzar al indexador de matriz a ser utilizado.
Lo cual es una tontería, por supuesto. Perfil solo código real .
La respuesta más rápida es usar C ++ / CLI: Cómo: acceder a los caracteres en un sistema :: cadena
Este enfoque itera a través de los caracteres en el lugar en la cadena usando la aritmética del puntero. No hay copias, no hay controles de rango implícitos y no hay llamadas a funciones por elemento.
Es posible obtener (casi, C ++ / CLI no requiere fijación) el mismo rendimiento de C # al escribir una versión insegura C # de PtrToStringChars
.
Algo como:
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();
}
Recuerde llamar a GCHandle.Free
después.
El comentario de CodeInChaos señala que C # proporciona un azúcar sintáctico para esto:
fixed(char* pch = s) { ... }
Si la optimización de micro es muy importante para usted, intente esto. (Supuse que la longitud de la cadena de entrada sería múltiplo de 8 para simplificar)
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++;
}
}
}
Es broma, nunca uses este código :)
Si la velocidad realmente importa es más rápido que foreach
for (int i = 0; i < text.Length; i++) {
char ch = text[i];
...
}
TL; DR: un foreach
simple es la forma más rápida de iterar una cadena.
Para las personas que vuelven a esto: ¡los tiempos cambian!
Con el último .NET 64 bit JIT, la versión insegura es la más lenta.
A continuación se muestra una implementación de referencia para BenchmarkDotNet. De estos, obtuve los siguientes resultados:
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 |
Los interesantes son los que no funcionan en una copia de matriz. Esto muestra que la indexación y los foreach
son muy similares en rendimiento, con una diferencia del 5%, foreach
es más rápido . Usar unsafe
es en realidad un 28% más lento que usar un foreach
.
En el pasado, unsafe
puede haber sido la opción más rápida, pero los JIT se vuelven más rápidos e inteligentes todo el tiempo.
Como referencia, el código de referencia:
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;
}
}
}
El código tiene algunos cambios menores del código ofrecido. Los caracteres que se recuperan de la cadena original son |
-ed con la variable que se devuelve, y devolvemos el valor. La razón de esto es que realmente necesitamos hacer algo con el resultado. De lo contrario, si solo estuviéramos iterando sobre la cadena como:
//8 foreach (c in string)
foreach (char c in longString) { }
el JIT es libre de eliminar esto porque podría inferir que en realidad no estás observando los resultados de la iteración. Por |
-al seleccionar los caracteres en la matriz y devolver esto, BenchmarkDotNet se asegurará de que el JIT no pueda realizar esta optimización.