tuple name dotnetperls .net performance tuples boxing design-decisions

.net - dotnetperls - tuple name items



.NET Tuple e igual rendimiento (1)

Esto es algo que no había notado hasta hoy. Aparentemente, la implementación en .NET de las clases de tuplas muy utilizadas ( Tuple<T> , Tuple<T1, T2> etc.) causa penalizaciones de boxeo para los tipos de valor cuando se realizan operaciones basadas en la igualdad.

Aquí es cómo la clase se implementa en el marco (fuente a través de ILSpy):

public class Tuple<T1, T2> : IStructuralEquatable { public T1 Item1 { get; private set; } public T2 Item2 { get; private set; } public Tuple(T1 item1, T2 item2) { this.Item1 = item1; this.Item2 = item2; } public override bool Equals(object obj) { return this.Equals(obj, EqualityComparer<object>.Default); } public override int GetHashCode() { return this.GetHashCode(EqualityComparer<object>.Default); } public bool Equals(object obj, IEqualityComparer comparer) { if (obj == null) { return false; } var tuple = obj as Tuple<T1, T2>; return tuple != null && comparer.Equals(this.Item1, tuple.Item1) && comparer.Equals(this.Item2, tuple.Item2); } public int GetHashCode(IEqualityComparer comparer) { int h1 = comparer.GetHashCode(this.Item1); int h2 = comparer.GetHashCode(this.Item2); return (h1 << 5) + h1 ^ h2; } }

El problema que veo es que causa un boxeo-unboxing de dos etapas, por ejemplo, para llamadas Equals , una, en el comparer.Equals qué caja del elemento, dos, EqualityComparer<object> llama a Equals no genéricos que a su vez internamente tendrán para desempaquetar el artículo al tipo original.

En cambio, ¿por qué no harían algo como:

public override bool Equals(object obj) { var tuple = obj as Tuple<T1, T2>; return tuple != null && EqualityComparer<T1>.Default.Equals(this.Item1, tuple.Item1) && EqualityComparer<T2>.Default.Equals(this.Item2, tuple.Item2); } public override int GetHashCode() { int h1 = EqualityComparer<T1>.Default.GetHashCode(this.Item1); int h2 = EqualityComparer<T2>.Default.GetHashCode(this.Item2); return (h1 << 5) + h1 ^ h2; } public bool Equals(object obj, IEqualityComparer comparer) { var tuple = obj as Tuple<T1, T2>; return tuple != null && comparer.Equals(this.Item1, tuple.Item1) && comparer.Equals(this.Item2, tuple.Item2); } public int GetHashCode(IEqualityComparer comparer) { int h1 = comparer.GetHashCode(this.Item1); int h2 = comparer.GetHashCode(this.Item2); return (h1 << 5) + h1 ^ h2; }

Me sorprendió ver la igualdad implementada de esta manera en la clase tuple .NET. Estaba usando el tipo de tupla como clave en uno de los diccionarios.

¿Hay alguna razón por la que esto deba implementarse como se muestra en el primer código? Es un poco desalentador hacer uso de esta clase en ese caso.

No creo que la refactorización de código y la no duplicación de datos hayan sido las principales preocupaciones. La misma implementación no genérica / boxeo se ha ido también detrás de IStructuralComparable , pero dado que IStructuralComparable.CompareTo se usa menos, no es un problema a menudo.

Comparé los dos enfoques anteriores con un tercer enfoque que aún es menos exigente, como este (solo lo esencial):

public override bool Equals(object obj) { return this.Equals(obj, EqualityComparer<T1>.Default, EqualityComparer<T2>.Default); } public bool Equals(object obj, IEqualityComparer comparer) { return this.Equals(obj, comparer, comparer); } private bool Equals(object obj, IEqualityComparer comparer1, IEqualityComparer comparer2) { var tuple = obj as Tuple<T1, T2>; return tuple != null && comparer1.Equals(this.Item1, tuple.Item1) && comparer2.Equals(this.Item2, tuple.Item2); }

para un par de campos Tuple<DateTime, DateTime> a 1000000 Equals calls. Este es el resultado:

1er enfoque (implementación .NET original) - 310 ms

2º enfoque - 60 ms

3er enfoque - 130 ms

La implementación predeterminada es aproximadamente 4-5 veces más lenta que la solución óptima.


Usted se preguntó si ''tiene que'' ser implementado de esa manera. En resumen, diría que no: hay muchas implementaciones funcionalmente equivalentes.

Pero, ¿por qué la implementación existente hace un uso tan explícito de EqualityComparer<object>.Default ? Puede que solo sea un caso de la persona que escribió esto optimizando mentalmente lo "incorrecto", o al menos algo diferente de su escenario de velocidad en un bucle interno. Dependiendo de su punto de referencia, puede parecer lo "correcto".

Pero, ¿qué escenario de referencia podría llevarlos a tomar esa decisión? Bueno, la optimización a la que se han dirigido parece ser la optimización para el número mínimo de instancias de plantilla de clase EqualityComparer. Es probable que elijan esto porque la creación de instancias de plantillas viene con costos de memoria o tiempo de carga. Si es así, podemos suponer que su escenario de referencia podría haberse basado en el tiempo de inicio de la aplicación o en el uso de la memoria en lugar de en un escenario de bucle cerrado.

Aquí hay un punto de conocimiento para respaldar la teoría (que se encuentra utilizando el sesgo de confirmación :): los cuerpos de los métodos de implementación de EqualityComparer no se pueden compartir si T es una estructura . Extraído de http://blogs.microsoft.co.il/sasha/2012/09/18/runtime-representation-of-genericspart-2/

Cuando el CLR necesita crear una instancia de un tipo genérico cerrado, como List, crea una tabla de métodos y una clase de EEC basada en el tipo abierto. Como siempre, la tabla de métodos contiene punteros a métodos, que el compilador JIT compila al vuelo. Sin embargo, aquí hay una optimización crucial: los cuerpos de los métodos compilados en tipos genéricos cerrados que tienen parámetros de tipo de referencia se pueden compartir. [...] La misma idea no funciona para tipos de valor. Por ejemplo, cuando T es larga, los elementos de la instrucción de asignación [tamaño] = elemento requieren una instrucción diferente, ya que se deben copiar 8 bytes en lugar de 4. Incluso los tipos de valores más grandes pueden requerir más de una instrucción; y así.