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)
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,
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.
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.
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
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.
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.
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.
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));
}
}
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.
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?
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???
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.