2010-06-25 21 views
29

Un consultor vino ayer y de alguna manera surgió el tema de las cadenas. Mencionó que había notado que para cadenas de menos de cierta longitud, Contains es en realidad más rápido que StartsWith. Tenía que verlo con mis propios ojos, así que escribí una pequeña aplicación y, efectivamente, ¡Contains es más rápido!Contiene es más rápido que StartsWith?

¿Cómo es esto posible?

DateTime start = DateTime.MinValue; 
DateTime end = DateTime.MinValue; 
string str = "Hello there"; 

start = DateTime.Now; 
for (int i = 0; i < 10000000; i++) 
{ 
    str.Contains("H"); 
} 
end = DateTime.Now; 
Console.WriteLine("{0}ms using Contains", end.Subtract(start).Milliseconds); 

start = DateTime.Now; 
for (int i = 0; i < 10000000; i++) 
{ 
    str.StartsWith("H"); 
} 
end = DateTime.Now; 
Console.WriteLine("{0}ms using StartsWith", end.Subtract(start).Milliseconds); 

Salidas:

726ms using Contains 
865ms using StartsWith 

Yo lo he probado con cadenas más largas también!

+2

Dos cosas. Intenta cambiar el orden para ver si afecta los resultados. Luego, dado que esta es una pregunta específica de la implementación, mire el código fuente, a través de Reflector si es necesario. Es probable que 'Contiene' se optimice más cuidadosamente (posiblemente usando código nativo) porque se usa con más frecuencia. –

+5

Las micro optimizaciones rara vez son útiles.Está comparando una cadena de una longitud máxima de aproximadamente 20 caracteres más de 10 millones de iteraciones y ahorrando unos ~ 140ms. Pruébelo con cadenas más largas o con un caso de uso más válido y vea si obtiene los mismos números. – Chris

+10

Sus medidas de tiempo son defectuosas. Debería utilizar un objeto de cronómetro para rastrear la hora, no DateTimes. Si va a utilizar DateTimes, al menos debería usar end.Subtract (start) .TotalMilliseconds –

Respuesta

25

Intente utilizar StopWatch para medir la velocidad en lugar de DateTime comprobación.

Stopwatch vs. using System.DateTime.Now for timing events

Creo que la clave es la siguiente las partes importantes en negrita:

Contains:

Este método realiza una cultura-ordinal (mayúsculas y minúsculas y comparación insensible).

StartsWith:

Este método realiza una palabra (mayúsculas y minúsculas y cultura sensible) comparación usando la cultura actual.

Creo que la clave es la comparación ordinal lo que equivale a:

Un tipo ordinal compara los cordajes de en el valor numérico de cada Char objeto en la cadena. Una comparación ordinal es automáticamente sensible a mayúsculas y minúsculas porque las minúsculas y las versiones en mayúsculas de un carácter tienen diferentes puntos de código. Sin embargo, si el caso no es importante en su aplicación , puede especificar una comparación ordinal que ignore el caso. Esto equivale a convertir la cadena en mayúsculas utilizando el cultivo invariante y luego realizar una comparación ordinal del resultado.

Referencias:

http://msdn.microsoft.com/en-us/library/system.string.aspx

http://msdn.microsoft.com/en-us/library/dy85x1sa.aspx

http://msdn.microsoft.com/en-us/library/baketfxw.aspx

El uso del reflector se puede ver el código para los dos:

public bool Contains(string value) 
{ 
    return (this.IndexOf(value, StringComparison.Ordinal) >= 0); 
} 

public bool StartsWith(string value, bool ignoreCase, CultureInfo culture) 
{ 
    if (value == null) 
    { 
     throw new ArgumentNullException("value"); 
    } 
    if (this == value) 
    { 
     return true; 
    } 
    CultureInfo info = (culture == null) ? CultureInfo.CurrentCulture : culture; 
    return info.CompareInfo.IsPrefix(this, value, 
     ignoreCase ? CompareOptions.IgnoreCase : CompareOptions.None); 
} 
+9

¡Sí! Esto es correcto. Como Daniel señaló en otro comentario, pasar StringComparison.Ordinal a StartsWith hará que StartsWith sea mucho más rápido que Contains. Lo probé y obtuve "748.3209ms usando Contiene 154.548ms usando StartsWith" – StriplingWarrior

+0

@StriplingWarrior, Cronómetro no es confiable ni con procesos cortos. Siempre habrá variaciones con cada prueba. Obtener 748 vs 154 ... ¡no es suficiente evidencia! Entonces la pregunta es, ¿Cuántas veces probaste tu prueba de proceso corta? – usefulBee

+1

@usefulBee: El código de la pregunta original repite la llamada al método diez millones de veces, lo que nos sitúa en cientos de milisegundos. Eso suele ser suficiente para suavizar las variaciones cuando no hay E/S involucradas. [Aquí hay un script de LINQPad] (http://share.linqpad.net/k7n66x.linq) que muestra resultados similares en un banco de pruebas de referencia más sólido. – StriplingWarrior

22

Lo descubrí. Es porque StartsWith es culturalmente sensible, mientras que Contains no lo es. Eso significa intrínsecamente que StartsWith tiene que hacer más trabajo.

Fwiw, aquí están mis resultados en Mono con el punto de referencia más abajo (corregido):

1988.7906ms using Contains 
10174.1019ms using StartsWith 

Estaría contento de ver los resultados de las personas sobre la EM, pero mi punto principal es que correctamente hecho (y suponiendo optimizaciones similares), creo StartsWith tiene que ser más lenta:

using System; 
using System.Diagnostics; 

public class ContainsStartsWith 
{ 
    public static void Main() 
    { 
     string str = "Hello there"; 

     Stopwatch s = new Stopwatch(); 
     s.Start(); 
     for (int i = 0; i < 10000000; i++) 
     { 
      str.Contains("H"); 
     } 
     s.Stop(); 
     Console.WriteLine("{0}ms using Contains", s.Elapsed.TotalMilliseconds); 

     s.Reset(); 
     s.Start(); 
     for (int i = 0; i < 10000000; i++) 
     { 
      str.StartsWith("H"); 
     } 
     s.Stop(); 
     Console.WriteLine("{0}ms using StartsWith", s.Elapsed.TotalMilliseconds); 

    } 
} 
+2

Muy buena conjetura, pero probablemente no. Él no está pasando por la cultura, y esta línea está en la implementación de StartsWith: 'CultureInfo info = (culture == null)? CultureInfo.CurrentCulture: culture; ' –

+2

@Marc Bollinger: todo lo que has demostrado es que StartsWith es sensible a la cultura, que es lo que se reclama. – Lee

+0

@Marc, a la derecha. Está usando la cultura actual. Eso es culturalmente sensible, y algunas culturas dependen de reglas de normalización bastante complejas. –

2

me hizo girar alrededor de reflector y encontré una posible respuesta:

Contiene:

return (this.IndexOf(value, StringComparison.Ordinal) >= 0); 

StartsWith:

... 
    switch (comparisonType) 
    { 
     case StringComparison.CurrentCulture: 
      return CultureInfo.CurrentCulture.CompareInfo.IsPrefix(this, value, CompareOptions.None); 

     case StringComparison.CurrentCultureIgnoreCase: 
      return CultureInfo.CurrentCulture.CompareInfo.IsPrefix(this, value, CompareOptions.IgnoreCase); 

     case StringComparison.InvariantCulture: 
      return CultureInfo.InvariantCulture.CompareInfo.IsPrefix(this, value, CompareOptions.None); 

     case StringComparison.InvariantCultureIgnoreCase: 
      return CultureInfo.InvariantCulture.CompareInfo.IsPrefix(this, value, CompareOptions.IgnoreCase); 

     case StringComparison.Ordinal: 
      return ((this.Length >= value.Length) && (nativeCompareOrdinalEx(this, 0, value, 0, value.Length) == 0)); 

     case StringComparison.OrdinalIgnoreCase: 
      return ((this.Length >= value.Length) && (TextInfo.CompareOrdinalIgnoreCaseEx(this, 0, value, 0, value.Length, value.Length) == 0)); 
    } 
    throw new ArgumentException(Environment.GetResourceString("NotSupported_StringComparison"), "comparisonType"); 

Y hay algunas sobrecargas para que la cultura por defecto es CurrentCulture.

En primer lugar, ordinal será más rápido (si la cadena está cerca del principio) de todos modos, ¿verdad? Y en segundo lugar, hay más lógica aquí que podría desacelerar las cosas (aunque es muy trivial)

+1

No estoy de acuerdo con que 'CultureInfo.CurrentCulture.CompareInfo.IsPrefix' sea trivial. –

+0

+1 - Realmente no lo leí para ser sincero, solo me refería a la gran cantidad de código;) – hackerhasid

9

StartsWith y Contains se comportan de forma completamente diferente cuando se trata de cuestiones relacionadas con la cultura.

En particular, StartsWith que devuelve NO implica Contains que devuelve true. Deberías reemplazar uno de ellos con el otro solo si realmente sabes lo que estás haciendo.

using System; 

class Program 
{ 
    static void Main() 
    { 
     var x = "A"; 
     var y = "A\u0640"; 

     Console.WriteLine(x.StartsWith(y)); // True 
     Console.WriteLine(x.Contains(y)); // False 
    } 
} 
0

Vamos a examinar lo que dice acerca de estos dos ILSpy ...

public virtual int IndexOf(string source, string value, int startIndex, int count, CompareOptions options) 
{ 
    if (source == null) 
    { 
     throw new ArgumentNullException("source"); 
    } 
    if (value == null) 
    { 
     throw new ArgumentNullException("value"); 
    } 
    if (startIndex > source.Length) 
    { 
     throw new ArgumentOutOfRangeException("startIndex", Environment.GetResourceString("ArgumentOutOfRange_Index")); 
    } 
    if (source.Length == 0) 
    { 
     if (value.Length == 0) 
     { 
      return 0; 
     } 
     return -1; 
    } 
    else 
    { 
     if (startIndex < 0) 
     { 
      throw new ArgumentOutOfRangeException("startIndex", Environment.GetResourceString("ArgumentOutOfRange_Index")); 
     } 
     if (count < 0 || startIndex > source.Length - count) 
     { 
      throw new ArgumentOutOfRangeException("count", Environment.GetResourceString("ArgumentOutOfRange_Count")); 
     } 
     if (options == CompareOptions.OrdinalIgnoreCase) 
     { 
      return source.IndexOf(value, startIndex, count, StringComparison.OrdinalIgnoreCase); 
     } 
     if ((options & ~(CompareOptions.IgnoreCase | CompareOptions.IgnoreNonSpace | CompareOptions.IgnoreSymbols | CompareOptions.IgnoreKanaType | CompareOptions.IgnoreWidth)) != CompareOptions.None && options != CompareOptions.Ordinal) 
     { 
      throw new ArgumentException(Environment.GetResourceString("Argument_InvalidFlag"), "options"); 
     } 
     return CompareInfo.InternalFindNLSStringEx(this.m_dataHandle, this.m_handleOrigin, this.m_sortName, CompareInfo.GetNativeCompareFlags(options) | 4194304 | ((source.IsAscii() && value.IsAscii()) ? 536870912 : 0), source, count, startIndex, value, value.Length); 
    } 
} 

parece que considera la cultura como bien, pero es por defecto.

public bool StartsWith(string value, StringComparison comparisonType) 
{ 
    if (value == null) 
    { 
     throw new ArgumentNullException("value"); 
    } 
    if (comparisonType < StringComparison.CurrentCulture || comparisonType > StringComparison.OrdinalIgnoreCase) 
    { 
     throw new ArgumentException(Environment.GetResourceString("NotSupported_StringComparison"), "comparisonType"); 
    } 
    if (this == value) 
    { 
     return true; 
    } 
    if (value.Length == 0) 
    { 
     return true; 
    } 
    switch (comparisonType) 
    { 
    case StringComparison.CurrentCulture: 
     return CultureInfo.CurrentCulture.CompareInfo.IsPrefix(this, value, CompareOptions.None); 
    case StringComparison.CurrentCultureIgnoreCase: 
     return CultureInfo.CurrentCulture.CompareInfo.IsPrefix(this, value, CompareOptions.IgnoreCase); 
    case StringComparison.InvariantCulture: 
     return CultureInfo.InvariantCulture.CompareInfo.IsPrefix(this, value, CompareOptions.None); 
    case StringComparison.InvariantCultureIgnoreCase: 
     return CultureInfo.InvariantCulture.CompareInfo.IsPrefix(this, value, CompareOptions.IgnoreCase); 
    case StringComparison.Ordinal: 
     return this.Length >= value.Length && string.nativeCompareOrdinalEx(this, 0, value, 0, value.Length) == 0; 
    case StringComparison.OrdinalIgnoreCase: 
     return this.Length >= value.Length && TextInfo.CompareOrdinalIgnoreCaseEx(this, 0, value, 0, value.Length, value.Length) == 0; 
    default: 
     throw new ArgumentException(Environment.GetResourceString("NotSupported_StringComparison"), "comparisonType"); 
    } 

Por el contrario, la única diferencia que veo que parece relevante es una verificación de longitud extra.

Cuestiones relacionadas