String o string en C#

En mis programas, acostumbraba a utilizar variables de tipo String, en vez de declararlas del tipo string. Mi razonamiento era que si string, simplemente es un tipo que encapsula a la clase System.String, entonces, iba a ser algo más eficiente utilizar directamente el segundo.

Cabe decir, que el MSDN, secunda esta hipótesis con la frase: "El tipo string representa una secuencia de cero o más caracteres Unicode. string es un alias de String en .NET Framework". Esto quiere decir, que sobre el papel, la implementación de String, debe ser más eficiente que la de string, lo cual se aplicará a todos los lenguajes de la plataforma .NET (VB.NET, C#, J#, …).

Para comprobarlo, he creado un sencillo programita en Visual C# 2008, que se encarga de iterar durante 500.000 veces, haciendo las operaciones más habituales contra una cadena de caracteres (trim, convertir a mayúsculas, convertir a minúsculas, concatenar, asignar, obtener la longitud, …). Realiza este proceso, usando primeramente String, y luego lo hace con string, calculando en ambos casos el tiempo necesario para hacerlo.

El programa, lo he compilado y ejecutado en un T2330 a 1,6 Ghz, usando el .NET Framework 3.5 en su versión de 32 bits, los tiempos obtenidos, han sido más que reveladores.

Primero de todo, la ejecución ha resultado más veloz usando el tipo, que usando la clase. 42.100.000 contra 42.400.000 ticks, es decir, un 1% a su favor, que en determinados casos, puede ser una diferencia suficientemente significativa. Pero lo más importante, es que esto me lleva a pensar, que probablemente, la implementación sea inversa a lo que afirma la propia Microsoft, y que en realidad es System.String el alias de string.

He consultado las definiciones de la clase String en System.String.cs, y lo que he visto es que los métodos que operan con cadenas, están declarados para usar el tipo nativo string. De nuevo, los indicios indican que es String el alias de string y no a la inversa. Lo siguiente es un extracto de la declaración de una sobrecarga del método Trim en la clase String:

using System.Collections;
using System.Collections.Generic;
using System.Globalization;
using System.Reflection;
using System.Runtime.ConstrainedExecution;
using System.Runtime.InteropServices;
using System.Text;
namespace System
{
[Serializable]
[ComVisible(true)]
public sealed class String : IComparable, ICloneable, IConvertible, IComparable<string>, IEnumerable<char>, IEnumerable, IEquatable<string>
{

public string Trim();
//
// Summary:
// Removes all leading and trailing occurrences of a set of characters specified
// in an array from the current System.String object.
//
// Parameters:
// trimChars:
// An array of Unicode characters to remove or null.
//
// Returns:
// The string that remains after all occurrences of the characters in the trimChars
// parameter are removed from the start and end of the current System.String
// object. If trimChars is null, white-space characters are removed instead.

}
}

En esta declaración, vemos que efectivamente, internamente se usa string. Así que para futuros desarrollos en C# ya sabéis, mejor usar string que String.

Como referencia, el código del programa completo, es el siguiente:

using System;
using System.Windows.Forms;
static class Program
{
static void Main()
{
int iCont;
String s1;
string s2;
long lInicio1, lFin1, lInicio2, lFin2;
lInicio1 = DateTime.Now.Ticks;
for (iCont = 500000; iCont > 0; iCont–)
{
s1 = "";
s1 = iCont + s1 + iCont;
s1 = s1 + (s1.Length);
s1 = s1.ToLower();
s1 = s1.ToUpper();
s1 = s1.Trim();
s1.Clone();
s1.LastIndexOf("1");
s1.IndexOf("1000");
s1.StartsWith("1000");
s1.EndsWith("1");
s1.Contains("1");
s1.Equals("1");
s1.Insert(0, "0");
s1.Remove(0);
}
lFin1 = DateTime.Now.Ticks;
lInicio2 = DateTime.Now.Ticks;
for (iCont = 500000; iCont > 0; iCont–)
{
s2 = "";
s2 = iCont + s2 + iCont;
s2 = s2 + (s2.Length);
s2 = s2.ToLower();
s2 = s2.ToUpper();
s2 = s2.Trim();
s2.Clone();
s2.LastIndexOf("1");
s2.IndexOf("1000");
s2.StartsWith("1000");
s2.EndsWith("1");
s2.Contains("1");
s2.Equals("1");
s2.Insert(0, "0");
s2.Remove(0);
}
lFin2 = DateTime.Now.Ticks;
MessageBox.Show("String: " + (lFin1 – lInicio1) + "\n" + "string: " + (lFin2 – lInicio2));
}
}

También podéis descargar el proyecto completo (incluyendo fuentes y binarios), aquí (4 Kb. en formato ZIP)


String o string en C#

12 comentarios en “String o string en C#”

  1. Hola Javier,

    Sólo una pequeña corrección:

    Declarar una variable con string es exactamente lo mismo que declararla con System.String pues string no es más que un alias. Para confirmarlo sólo tienes que declarar una variable con cada opción y ver el código MSIL generado por el compilador. Es el mismo caso que int frente a System.Int32 y el resto de alias para tipos básicos.
    Otra forma de verlo es que si usas string el Visual Studio te lo resalta como palabra clave del lenguage, mientras que System.String no.

    Un saludo,

  2. Javier Gutiérrez Chamorro (Guti)

    El lo que yo pensaba Carlos, sin embargo al revisar la implementación de System.String, he visto que realmente está basada en tipos string, por lo tanto, en caso de ser un alias, lo es al revés de lo que dice la documentación.

    Ante esta hipótesis, me extraña la diferencia de rendimiento entre una y otra, pues si la implementación es idéntica, la velocidad de ejecución también debería serlo.

  3. La especificación del lenguaje (c# 3.0) dice eso, string es un alias a System.String, por lo que no tendría que existir ninguna diferencia.

    La prueba definitiva es mirar el código generado final que es interpretado, que según dice Carlos es igual, y yo le creo.

    En mi pc, compilando con mono 1.2.6, ejecuto varias veces y obtengo resultados para ambos prácticamente idénticos:

    $ ./Program.exe
    String: 48733700
    string: 48994560
    blaxter@helicon:~/tmp$ ./Program.exe
    String: 50121350
    string: 49703990
    blaxter@helicon:~/tmp$ ./Program.exe
    String: 47922880
    string: 48098590

    Son operaciones con coste prácticamente nulo (500 mil veces 15 operaciones sobre strings diminutas…) por lo que no es que sea un "benchmark" muy interesante que digamos.

  4. Javier Gutiérrez Chamorro (Guti)

    Blaxter, en cuanto a que el benchmark no es bueno, mi opinión es que si lo es, si lo que se quería valorar era el impacto de llamar a un tipo de objeto u otro, por ello se evitaron operaciones largas, para maximizar este efecto.

    En todo caso, a raiz de tus resultados tan parejos sobre mono, he procedido a desensamblar el bytecode como sugería Carlos; efectivamente el código generado es idéntico en ambos casos, por lo que no me explico porque en los diferentes equipos dónde lo probé, con Vista y XP, algunos x64, y otros x86, unos con CPU Intel, otros AMD, siempre obtenía ventaja el segundo.

    Si tenéis curiosidad, el MSIL es este:

    .method private hidebysig static void Main() cil managed
    {
    .entrypoint
    // Code size 502 (0x1f6)
    .maxstack 4
    .locals init (int32 V_0,
    string V_1,
    string V_2,
    int64 V_3,
    int64 V_4,
    int64 V_5,
    int64 V_6,
    valuetype [mscorlib]System.DateTime V_7,
    valuetype [mscorlib]System.DateTime V_8,
    valuetype [mscorlib]System.DateTime V_9,
    valuetype [mscorlib]System.DateTime V_10,
    object[] V_11)
    IL_0000: call valuetype [mscorlib]System.DateTime [mscorlib]System.DateTime::get_Now()
    IL_0005: stloc.s V_7
    IL_0007: ldloca.s V_7
    IL_0009: call instance int64 [mscorlib]System.DateTime::get_Ticks()
    IL_000e: stloc.3
    IL_000f: ldc.i4 0x7a120
    IL_0014: stloc.0
    IL_0015: br IL_00c2
    IL_001a: ldstr ""
    IL_001f: stloc.1
    IL_0020: ldloc.0
    IL_0021: box [mscorlib]System.Int32
    IL_0026: ldloc.1
    IL_0027: ldloc.0
    IL_0028: box [mscorlib]System.Int32
    IL_002d: call string [mscorlib]System.String::Concat(object,
    object,
    object)
    IL_0032: stloc.1
    IL_0033: ldloc.1
    IL_0034: ldloc.1
    IL_0035: callvirt instance int32 [mscorlib]System.String::get_Length()
    IL_003a: box [mscorlib]System.Int32
    IL_003f: call string [mscorlib]System.String::Concat(object,
    object)
    IL_0044: stloc.1
    IL_0045: ldloc.1
    IL_0046: callvirt instance string [mscorlib]System.String::ToLower()
    IL_004b: stloc.1
    IL_004c: ldloc.1
    IL_004d: callvirt instance string [mscorlib]System.String::ToUpper()
    IL_0052: stloc.1
    IL_0053: ldloc.1
    IL_0054: callvirt instance string [mscorlib]System.String::Trim()
    IL_0059: stloc.1
    IL_005a: ldloc.1
    IL_005b: callvirt instance object [mscorlib]System.String::Clone()
    IL_0060: pop
    IL_0061: ldloc.1
    IL_0062: ldstr "1"
    IL_0067: callvirt instance int32 [mscorlib]System.String::LastIndexOf(string)
    IL_006c: pop
    IL_006d: ldloc.1
    IL_006e: ldstr "1000"
    IL_0073: callvirt instance int32 [mscorlib]System.String::IndexOf(string)
    IL_0078: pop
    IL_0079: ldloc.1
    IL_007a: ldstr "1000"
    IL_007f: callvirt instance bool [mscorlib]System.String::StartsWith(string)
    IL_0084: pop
    IL_0085: ldloc.1
    IL_0086: ldstr "1"
    IL_008b: callvirt instance bool [mscorlib]System.String::EndsWith(string)
    IL_0090: pop
    IL_0091: ldloc.1
    IL_0092: ldstr "1"
    IL_0097: callvirt instance bool [mscorlib]System.String::Contains(string)
    IL_009c: pop
    IL_009d: ldloc.1
    IL_009e: ldstr "1"
    IL_00a3: callvirt instance bool [mscorlib]System.String::Equals(string)
    IL_00a8: pop
    IL_00a9: ldloc.1
    IL_00aa: ldc.i4.0
    IL_00ab: ldstr "0"
    IL_00b0: callvirt instance string [mscorlib]System.String::Insert(int32,
    string)
    IL_00b5: pop
    IL_00b6: ldloc.1
    IL_00b7: ldc.i4.0
    IL_00b8: callvirt instance string [mscorlib]System.String::Remove(int32)
    IL_00bd: pop
    IL_00be: ldloc.0
    IL_00bf: ldc.i4.1
    IL_00c0: sub
    IL_00c1: stloc.0
    IL_00c2: ldloc.0
    IL_00c3: ldc.i4.0
    IL_00c4: bgt IL_001a
    IL_00c9: call valuetype [mscorlib]System.DateTime [mscorlib]System.DateTime::get_Now()
    IL_00ce: stloc.s V_8
    IL_00d0: ldloca.s V_8
    IL_00d2: call instance int64 [mscorlib]System.DateTime::get_Ticks()
    IL_00d7: stloc.s V_4
    IL_00d9: call valuetype [mscorlib]System.DateTime [mscorlib]System.DateTime::get_Now()
    IL_00de: stloc.s V_9
    IL_00e0: ldloca.s V_9
    IL_00e2: call instance int64 [mscorlib]System.DateTime::get_Ticks()
    IL_00e7: stloc.s V_5
    IL_00e9: ldc.i4 0x7a120
    IL_00ee: stloc.0
    IL_00ef: br IL_019c
    IL_00f4: ldstr ""
    IL_00f9: stloc.2
    IL_00fa: ldloc.0
    IL_00fb: box [mscorlib]System.Int32
    IL_0100: ldloc.2
    IL_0101: ldloc.0
    IL_0102: box [mscorlib]System.Int32
    IL_0107: call string [mscorlib]System.String::Concat(object,
    object,
    object)
    IL_010c: stloc.2
    IL_010d: ldloc.2
    IL_010e: ldloc.2
    IL_010f: callvirt instance int32 [mscorlib]System.String::get_Length()
    IL_0114: box [mscorlib]System.Int32
    IL_0119: call string [mscorlib]System.String::Concat(object,
    object)
    IL_011e: stloc.2
    IL_011f: ldloc.2
    IL_0120: callvirt instance string [mscorlib]System.String::ToLower()
    IL_0125: stloc.2
    IL_0126: ldloc.2
    IL_0127: callvirt instance string [mscorlib]System.String::ToUpper()
    IL_012c: stloc.2
    IL_012d: ldloc.2
    IL_012e: callvirt instance string [mscorlib]System.String::Trim()
    IL_0133: stloc.2
    IL_0134: ldloc.2
    IL_0135: callvirt instance object [mscorlib]System.String::Clone()
    IL_013a: pop
    IL_013b: ldloc.2
    IL_013c: ldstr "1"
    IL_0141: callvirt instance int32 [mscorlib]System.String::LastIndexOf(string)
    IL_0146: pop
    IL_0147: ldloc.2
    IL_0148: ldstr "1000"
    IL_014d: callvirt instance int32 [mscorlib]System.String::IndexOf(string)
    IL_0152: pop
    IL_0153: ldloc.2
    IL_0154: ldstr "1000"
    IL_0159: callvirt instance bool [mscorlib]System.String::StartsWith(string)
    IL_015e: pop
    IL_015f: ldloc.2
    IL_0160: ldstr "1"
    IL_0165: callvirt instance bool [mscorlib]System.String::EndsWith(string)
    IL_016a: pop
    IL_016b: ldloc.2
    IL_016c: ldstr "1"
    IL_0171: callvirt instance bool [mscorlib]System.String::Contains(string)
    IL_0176: pop
    IL_0177: ldloc.2
    IL_0178: ldstr "1"
    IL_017d: callvirt instance bool [mscorlib]System.String::Equals(string)
    IL_0182: pop
    IL_0183: ldloc.2
    IL_0184: ldc.i4.0
    IL_0185: ldstr "0"
    IL_018a: callvirt instance string [mscorlib]System.String::Insert(int32,
    string)
    IL_018f: pop
    IL_0190: ldloc.2
    IL_0191: ldc.i4.0
    IL_0192: callvirt instance string [mscorlib]System.String::Remove(int32)
    IL_0197: pop
    IL_0198: ldloc.0
    IL_0199: ldc.i4.1
    IL_019a: sub
    IL_019b: stloc.0
    IL_019c: ldloc.0
    IL_019d: ldc.i4.0
    IL_019e: bgt IL_00f4
    IL_01a3: call valuetype [mscorlib]System.DateTime [mscorlib]System.DateTime::get_Now()
    IL_01a8: stloc.s V_10
    IL_01aa: ldloca.s V_10
    IL_01ac: call instance int64 [mscorlib]System.DateTime::get_Ticks()
    IL_01b1: stloc.s V_6
    IL_01b3: ldc.i4.4
    IL_01b4: newarr [mscorlib]System.Object
    IL_01b9: stloc.s V_11
    IL_01bb: ldloc.s V_11
    IL_01bd: ldc.i4.0
    IL_01be: ldstr "String: "
    IL_01c3: stelem.ref
    IL_01c4: ldloc.s V_11
    IL_01c6: ldc.i4.1
    IL_01c7: ldloc.s V_4
    IL_01c9: ldloc.3
    IL_01ca: sub
    IL_01cb: box [mscorlib]System.Int64
    IL_01d0: stelem.ref
    IL_01d1: ldloc.s V_11
    IL_01d3: ldc.i4.2
    IL_01d4: ldstr "\nstring: "
    IL_01d9: stelem.ref
    IL_01da: ldloc.s V_11
    IL_01dc: ldc.i4.3
    IL_01dd: ldloc.s V_6
    IL_01df: ldloc.s V_5
    IL_01e1: sub
    IL_01e2: box [mscorlib]System.Int64
    IL_01e7: stelem.ref
    IL_01e8: ldloc.s V_11
    IL_01ea: call string [mscorlib]System.String::Concat(object[])
    IL_01ef: call valuetype [System.Windows.Forms]System.Windows.Forms.DialogResult [System.Windows.Forms]System.Windows.Forms.MessageBox::Show(string)
    IL_01f4: pop
    IL_01f5: ret
    } // end of method Program::Main

  5. Este codigo (VB)

    Private Sub Form1_Load(ByVal sender As Object, ByVal e As EventArgs) Handles MyBase.Load
    For i As Integer = 0 To 9
    Test()
    Next
    Me.Close()
    End Sub
    Sub Test()
    Dim a As Long
    Dim t As Long = Now.Ticks
    For i As Integer = 0 To 100000000
    a = a + i
    Next
    MsgBox(Now.Ticks – t)
    End Sub

    me da entre 5000000 y 5312500 ticks, es decir una diferencia de 312500 ticks (31 ms). El problema es la falta de precisión de la propiedad ticks.

  6. Con respecto a mi comentario anterior, probe el mismo código con Diagnostics.Stopwatch y obtuve fluctuaciones similares, así que probablemente los culpables son los procesos de Windows y lo que sea que haga el CLR con la memoria.

  7. Javier Gutiérrez Chamorro (Guti)

    Gracias por tu contribución Guillermo. Realmente veo cosas muy raras… Sobre el tema de los procesos en ejecución, me extrañaría que fuera el causante, de ser así, el ejecutable debería haber conseguido ser más rápido, al menos una vez usando String que string, y en mis pruebas, eso no ha pasado nunca.

    En cuanto a la gestión de memoria del CLR, era una de las opciones que manejaba, pero en ese caso, ¿por qué es el segundo el que va más rápido?, debería ser el primero.

    Tienes mucha razón en la precisión de Ticks, que directamente o indirectamente no es muy alta, no obstante, es un condicionante que debería afectar por igual a ambas implementaciones.

    Por ahora, mis hipótesis van en la linea que la primera ejecución, ha permitido cachear algo, no se si los datos o el código que usa el segundo algoritmo, de manera que la segunda versión es la que se beneficia de ello.

  8. Javier Gutiérrez Chamorro (Guti)

    El asunto sigue siendo enigmático para mi… Reordenando el programa, de manera que primero se ejecute la comparativa con string, y luego con String, para verificar la hipótesis de código o datos cacheados en la CPU, he de decir, que el resultado ha vuelto a ser el mismo.

    A pesar de que ahora es string la que se ejecuta primero, y luego va String, sigue ganando levemente la construcción con string… Algo muy raro de hecho…

    Os dejo el código modificado a tal efecto:
    using System;
    using System.Windows.Forms;

    static class Program
    {
    static void Main()
    {
    int iCont;
    string s2;
    String s1;
    long lInicio2, lFin2, lInicio1, lFin1;

    lInicio2 = DateTime.Now.Ticks;
    for (iCont = 500000; iCont > 0; iCont–)
    {
    s2 = "";
    s2 = iCont + s2 + iCont;
    s2 = s2 + (s2.Length);
    s2 = s2.ToLower();
    s2 = s2.ToUpper();
    s2 = s2.Trim();
    s2.Clone();
    s2.LastIndexOf("1");
    s2.IndexOf("1000");
    s2.StartsWith("1000");
    s2.EndsWith("1");
    s2.Contains("1");
    s2.Equals("1");
    s2.Insert(0, "0");
    s2.Remove(0);
    }
    lFin2 = DateTime.Now.Ticks;

    lInicio1 = DateTime.Now.Ticks;
    for (iCont = 500000; iCont > 0; iCont–)
    {
    s1 = "";
    s1 = iCont + s1 + iCont;
    s1 = s1 + (s1.Length);
    s1 = s1.ToLower();
    s1 = s1.ToUpper();
    s1 = s1.Trim();
    s1.Clone();
    s1.LastIndexOf("1");
    s1.IndexOf("1000");
    s1.StartsWith("1000");
    s1.EndsWith("1");
    s1.Contains("1");
    s1.Equals("1");
    s1.Insert(0, "0");
    s1.Remove(0);
    }
    lFin1 = DateTime.Now.Ticks;

    MessageBox.Show("String: " + (lFin1 – lInicio1) + "\n" + "string: " + (lFin2 – lInicio2));
    }
    }

  9. A mi me fluctúa hacia ambos lados. Si multiplico el número de iteraciones por 10 veces obtengo valores menores para el que vaya primero.
    De cualquier manera si se va a hacer millones de operaciones con String quizás sea mejor usar StringBuilder.

  10. Javier Gutiérrez Chamorro (Guti)

    Realmente curioso que en tu caso siempre venza el primero Guillermo. Habrá que investigar que hace exactamente por dentro el CLR… ¿O quizás tenga que ver con el hardware de la CPU?

  11. Habéis revisado el MSIL para este tema???
    ¿Habeis probado a cambiar de orden que se ejecute primero String y luego string o viceversa? Yo lo he hecho y siempre el más rapido es el segundo y puede ser por mil motivos la mayoria de ellos basados en que no es una maquina virtual ni un micro dedicado. ¿Que hara la maquina virtual para atender las peticiones de traducción? y el gestor de memoria ejecutandose en background? y el sistema operativo repartiendo más o menos micro???

  12. Javier Gutiérrez Chamorro (Guti)

    Muy acertado tu punto de vista sobre la carga que pueda tener la máquina física en cada momento, cosa determinante cuando las diferencias son tan leves.

    Han pasado más de 4 años, y doy por sentado que el JIT en .NET 4.5, utilizará técnicas de inlining, y a efectos prácticos String y string deben ser equivalentes a nivel de rendimiento, pero no lo he comprobado de primera mano.

    El código MSIL, bueno ahora CIL, lo tienes en el 4º comentario.

Deja un comentario