c# - genericos - Rendimiento: tipo derivado de genérico
metodos genericos c# (3)
Después de algunos experimentos, encontré que Enumerable.Empty<T>
siempre es lento cuando T es un tipo de clase ; si es un tipo de valor, es más rápido, pero depende del tamaño de la estructura. Probé object, string, int, PointF, RectangleF, DateTime, Guid.
Mirando cómo se implementa, probé diferentes alternativas y encontré algunas que funcionan rápido.
Enumerable.Empty<T>
basa en la propiedad estática de Instance
la clase interna EmptyEnumerable<TElement>
.
Esa propiedad hace pequeñas cosas:
- Comprueba si un campo volátil estático privado es nulo.
- Asigna una matriz vacía al campo una vez (solo si está vacía).
- Devuelve el valor del campo.
Entonces, lo que Enumerable.Empty<T>
realmente está haciendo es devolver una matriz vacía de T.
Probando diferentes enfoques, encontré que la lentitud es causada tanto por la propiedad como por el modificador volátil .
Adoptando un campo estático inicializado a T [0] en lugar de Enumerable.Empty<T>
like
public static readonly T[] EmptyArray = new T[0];
el problema desapareció Tenga en cuenta que el modificador de solo lectura no es determinante. Tener el mismo campo estático declarado con volátiles o acceder a través de una propiedad causa el problema.
Saludos, Daniele.
Me he encontrado con un problema de rendimiento que no puedo entender del todo. Sé cómo solucionarlo, pero no entiendo por qué sucede eso. ¡Es solo por diversion!
Hablemos del código. Simplifiqué el código tanto como pude para reproducir el problema.
Supongamos que tenemos una clase genérica. Tiene una lista vacía adentro y hace algo con T
en constructor. Tiene el método Run
que llama a un método IEnumerable<T>
en la lista, por ejemplo, Any()
.
public class BaseClass<T>
{
private List<T> _list = new List<T>();
public BaseClass()
{
Enumerable.Empty<T>();
// or Enumerable.Repeat(new T(), 10);
// or even new T();
// or foreach (var item in _list) {}
}
public void Run()
{
for (var i = 0; i < 8000000; i++)
{
if (_list.Any())
// or if (_list.Count() > 0)
// or if (_list.FirstOrDefault() != null)
// or if (_list.SingleOrDefault() != null)
// or other IEnumerable<T> method
{
return;
}
}
}
}
Entonces tenemos una clase derivada que está vacía:
public class DerivedClass : BaseClass<object>
{
}
ClassBase<T>.Run
el rendimiento de ejecutar el ClassBase<T>.Run
de ambas clases. El acceso desde el tipo derivado es 4 veces más lento que desde la clase base. Y no puedo entender por qué sucede eso. Compilado en modo Release, el resultado es el mismo con calentamiento. Sucede solo en .NET 4.5.
public class Program
{
public static void Main()
{
Measure(new DerivedClass());
Measure(new BaseClass<object>());
}
private static void Measure(BaseClass<object> baseClass)
{
var sw = Stopwatch.StartNew();
baseClass.Run();
sw.Stop();
Console.WriteLine(sw.ElapsedMilliseconds);
}
}
Listado completo en esencia
Parece que hay un problema de optimizador de CLR. Desactive el "Código de Optimize" en la pestaña de compilación e intente ejecutar su prueba nuevamente.
Actualizar:
Hay una respuesta del equipo de CLR en Microsoft Connect
Está relacionado con búsquedas de diccionarios en códigos genéricos compartidos. La heurística en tiempo de ejecución y JIT no funcionan bien para esta prueba en particular. Vamos a ver qué se puede hacer al respecto.
Mientras tanto, puede solucionarlo agregando dos métodos ficticios a la BaseClass (ni siquiera necesita ser llamado). Hará que la heurística funcione como uno esperaría.
Original:
Eso es JIT fail.
Puede arreglarse con esta locura:
public class BaseClass<T>
{
private List<T> _list = new List<T>();
public BaseClass()
{
Enumerable.Empty<T>();
// or Enumerable.Repeat(new T(), 10);
// or even new T();
// or foreach (var item in _list) {}
}
public void Run()
{
for (var i = 0; i < 8000000; i++)
{
if (_list.Any())
{
return;
}
}
}
public void Run2()
{
for (var i = 0; i < 8000000; i++)
{
if (_list.Any())
{
return;
}
}
}
public void Run3()
{
for (var i = 0; i < 8000000; i++)
{
if (_list.Any())
{
return;
}
}
}
}
Tenga en cuenta que Run2 () / Run3 () no se invocan desde ningún lugar. Pero si comenta los métodos Run2 o Run3, obtendrá una penalización de rendimiento como antes.
Hay algo relacionado con la alineación de la pila o con el tamaño de la tabla de métodos, supongo.
PD: puedes reemplazar
Enumerable.Empty<T>();
// with
var x = new Func<IEnumerable<T>>(Enumerable.Empty<T>);
sigue siendo el mismo error.