¿Por qué el nuevo tipo de tupla en .Net 4.0 es un tipo de referencia (clase) y no un tipo de valor (estructura)?

89

¿Alguien sabe la respuesta y / o tiene una opinión al respecto?

Dado que las tuplas normalmente no serían muy grandes, supongo que tendría más sentido usar estructuras que clases para estas. ¿Lo que usted dice?

Bent Rasmussen
fuente
1
Para cualquiera que se encuentre aquí después de 2016. En c # 7 y versiones posteriores, los literales Tuple son de la familia de tipos ValueTuple<...>. Consulte la referencia en Tipos de tuplas de C #
Tamir Daniely

Respuestas:

94

Microsoft hizo todos los tipos de tuplas tipos de referencia en aras de la simplicidad.

Personalmente, creo que esto fue un error. Las tuplas con más de 4 campos son muy inusuales y, de todos modos, deben reemplazarse con una alternativa con más tipos (como un tipo de registro en F #), por lo que solo las tuplas pequeñas son de interés práctico. Mis propios puntos de referencia mostraron que las tuplas sin caja de hasta 512 bytes aún podrían ser más rápidas que las tuplas en caja.

Aunque la eficiencia de la memoria es una preocupación, creo que el problema dominante es la sobrecarga del recolector de basura .NET. La asignación y la recolección son muy costosas en .NET porque su recolector de basura no ha sido muy optimizado (por ejemplo, en comparación con la JVM). Además, la estación de trabajo .NET GC (estación de trabajo) predeterminada aún no se ha paralelizado. En consecuencia, los programas paralelos que utilizan tuplas se detienen cuando todos los núcleos compiten por el recolector de basura compartido, lo que destruye la escalabilidad. Esta no es solo la preocupación dominante, sino que, AFAIK, fue completamente ignorada por Microsoft cuando examinaron este problema.

Otra preocupación es el envío virtual. Los tipos de referencia admiten subtipos y, por lo tanto, sus miembros generalmente se invocan a través de un envío virtual. Por el contrario, los tipos de valor no pueden admitir subtipos, por lo que la invocación de miembros es completamente inequívoca y siempre se puede realizar como una llamada de función directa. El envío virtual es enormemente caro en el hardware moderno porque la CPU no puede predecir dónde terminará el contador del programa. La JVM hace todo lo posible para optimizar el despacho virtual, pero .NET no. Sin embargo, .NET proporciona un escape del envío virtual en forma de tipos de valor. Entonces, representar tuplas como tipos de valor podría, nuevamente, haber mejorado dramáticamente el rendimiento aquí. Por ejemplo, llamandoGetHashCode en una tupla de 2, un millón de veces toma 0,17 s, pero llamarlo en una estructura equivalente toma solo 0,008 s, es decir, el tipo de valor es 20 veces más rápido que el tipo de referencia.

Una situación real en la que surgen comúnmente estos problemas de rendimiento con tuplas es en el uso de tuplas como claves en diccionarios. De hecho, me topé con este hilo siguiendo un enlace de la pregunta de desbordamiento de pila F # ejecuta mi algoritmo más lento que Python. donde el programa F # del autor resultó ser más lento que su Python precisamente porque estaba usando tuplas en caja. El desempaquetado manual con un structtipo escrito a mano hace que su programa F # sea varias veces más rápido y más rápido que Python. Estos problemas nunca hubieran surgido si las tuplas estuvieran representadas por tipos de valor y no por tipos de referencia para empezar ...

JD
fuente
2
@Bent: Sí, eso es exactamente lo que hago cuando me encuentro con tuplas en una ruta activa en F #. Sin embargo, sería bueno si hubieran proporcionado tuplas en caja y sin caja en .NET Framework ...
JD
18
En cuanto al despacho virtual, creo que su culpa está fuera de lugar: los Tuple<_,...,_>tipos podrían haberse sellado, en cuyo caso no se necesitaría ningún despacho virtual a pesar de ser tipos de referencia. Tengo más curiosidad por saber por qué no están sellados que por qué son tipos de referencia.
kvb
2
Según mis pruebas, para el escenario en el que una tupla se generaría en una función y se devolvería a otra función, y luego nunca se volvería a usar, las estructuras de campo expuesto parecen ofrecer un rendimiento superior para cualquier elemento de datos de tamaño que no sea tan grande como para explotar la pila. Las clases inmutables solo son mejores si las referencias se transmitirán lo suficiente para justificar su costo de construcción (cuanto mayor sea el elemento de datos, menos tendrán que pasarse para que la compensación las favorezca). Dado que se supone que una tupla representa simplemente un grupo de variables unidas, una estructura parecería ideal.
supercat
2
"Las tuplas sin caja de hasta 512 bytes podrían ser más rápidas que las empaquetadas" , ¿cuál es ese escenario? Es posible que pueda asignar una estructura de 512B más rápido que una instancia de clase que contiene 512B de datos, pero pasarla sería más de 100 veces más lento (suponiendo x86). ¿Hay algo que estoy pasando por alto?
Groo
45

La razón más probable es que solo las tuplas más pequeñas tendrían sentido como tipos de valor, ya que tendrían una pequeña huella de memoria. Las tuplas más grandes (es decir, las que tienen más propiedades) en realidad sufrirían en rendimiento, ya que serían más grandes que 16 bytes.

En lugar de que algunas tuplas sean tipos de valor y otras tipos de referencia y obliguen a los desarrolladores a saber cuáles son cuáles, me imagino que la gente de Microsoft pensó que hacerlos todos los tipos de referencia era más simple.

¡Ah, sospechas confirmadas! Consulte Creación de tupla :

La primera decisión importante fue si tratar las tuplas como referencia o como tipo de valor. Dado que son inmutables cada vez que desee cambiar los valores de una tupla, debe crear una nueva. Si son tipos de referencia, esto significa que puede generarse mucha basura si está cambiando elementos en una tupla en un bucle cerrado. Las tuplas F # eran tipos de referencia, pero el equipo tenía la sensación de que podrían lograr una mejora del rendimiento si dos, y quizás tres, tuplas de elementos fueran tipos de valor. Algunos equipos que habían creado tuplas internas habían utilizado valor en lugar de tipos de referencia, porque sus escenarios eran muy sensibles a la creación de muchos objetos administrados. Descubrieron que el uso de un tipo de valor les daba un mejor rendimiento. En nuestro primer borrador de la especificación de tuplas, mantuvimos las tuplas de dos, tres y cuatro elementos como tipos de valor, siendo el resto tipos de referencia. Sin embargo, durante una reunión de diseño que incluyó representantes de otros idiomas, se decidió que este diseño "dividido" sería confuso, debido a la semántica ligeramente diferente entre los dos tipos. Se determinó que la coherencia en el comportamiento y el diseño era de mayor prioridad que los posibles aumentos de rendimiento. Basándonos en esta entrada, cambiamos el diseño para que todas las tuplas sean tipos de referencia, aunque le pedimos al equipo de F # que hiciera una investigación de rendimiento para ver si experimentó una aceleración al usar un tipo de valor para algunos tamaños de tuplas. Tenía una buena forma de probar esto, ya que su compilador, escrito en F #, fue un buen ejemplo de un programa grande que usaba tuplas en una variedad de escenarios. Al final, el equipo de F # descubrió que no mejoraba el rendimiento cuando algunas tuplas eran tipos de valor en lugar de tipos de referencia. Esto nos hizo sentir mejor acerca de nuestra decisión de usar tipos de referencia para tuplas.

Andrew Hare
fuente
3
Gran discusión aquí: blogs.msdn.com/bclteam/archive/2009/07/07/…
Keith Adler
Ahh ya veo. Todavía estoy un poco confundido de que los tipos de valores no significan nada en la práctica aquí: P
Bent Rasmussen
Acabo de leer el comentario sobre interfaces no genéricas y cuando miré el código anteriormente, eso fue exactamente otra cosa que me llamó la atención. Es realmente poco inspirador lo poco genéricos que son los tipos Tuple. Pero, supongo que siempre puedes crear el tuyo propio ... De todos modos, no hay soporte sintáctico en C #. Sin embargo, al menos ... Aún así, el uso de genéricos y las restricciones que tiene todavía se sienten limitados en .Net. Existe un potencial sustancial para bibliotecas muy abstractas muy genéricas, pero los genéricos probablemente necesiten cosas adicionales como tipos de retorno covariantes.
Bent Rasmussen
7
Su límite de "16 bytes" es falso. Cuando probé esto en .NET 4, descubrí que el GC es tan lento que las tuplas sin caja de hasta 512 bytes aún pueden ser más rápidas. También cuestionaría los resultados de referencia de Microsoft. Apuesto a que ignoraron el paralelismo (el compilador de F # no es paralelo) y ahí es donde evitar GC realmente vale la pena porque la estación de trabajo GC de .NET tampoco es paralela.
JD
Por curiosidad, me pregunto si el equipo del compilador probó la idea de hacer que las tuplas sean estructuras EXPOSED-FIELD . Si uno tiene una instancia de un tipo con varios rasgos, y necesita una instancia que sea idéntica excepto por un rasgo que es diferente, una estructura de campo expuesto puede lograrlo mucho más rápido que cualquier otro tipo, y la ventaja solo crece a medida que se obtienen las estructuras. más grande.
supercat
7

Si los tipos .NET System.Tuple <...> se definieran como estructuras, no serían escalables. Por ejemplo, una tupla ternaria de enteros largos se escala actualmente de la siguiente manera:

type Tuple3 = System.Tuple<int64, int64, int64>
type Tuple33 = System.Tuple<Tuple3, Tuple3, Tuple3>
sizeof<Tuple3> // Gets 4
sizeof<Tuple33> // Gets 4

Si la tupla ternaria se definiera como una estructura, el resultado sería el siguiente (basado en un ejemplo de prueba que implementé):

sizeof<Tuple3> // Would get 32
sizeof<Tuple33> // Would get 104

Como las tuplas tienen soporte de sintaxis incorporado en F #, y se usan con mucha frecuencia en este lenguaje, las tuplas "struct" pondrían a los programadores de F # en riesgo de escribir programas ineficientes sin siquiera ser conscientes de ello. Sucedería tan fácilmente:

let t3 = 1L, 2L, 3L
let t33 = t3, t3, t3

En mi opinión, las tuplas de "estructura" causarían una alta probabilidad de crear ineficiencias significativas en la programación diaria. Por otro lado, las tuplas de "clase" actualmente existentes también causan ciertas ineficiencias, como lo menciona @Jon. Sin embargo, creo que el producto de la "probabilidad de ocurrencia" por el "daño potencial" sería mucho más alto con estructuras de lo que es actualmente con clases. Por lo tanto, la implementación actual es el mal menor.

Idealmente, habría tuplas de "clase" y tuplas de "estructura", ¡ambas con soporte sintáctico en F #!

Editar (2017-10-07)

Las tuplas de estructuras ahora son totalmente compatibles de la siguiente manera:

Marc Sigrist
fuente
2
Si se evita la copia innecesaria, una estructura de campo expuesto de cualquier tamaño será más eficiente que una clase inmutable del mismo tamaño, a menos que cada instancia se copie suficientes veces para que el costo de dicha copia supere el costo de crear un objeto de montón (el el número de copias de equilibrio varía con el tamaño del objeto). Tal copia puede ser inevitable si uno quiere una estructura que pretende ser inmutable, pero las estructuras que están diseñadas para aparecer como colecciones de variables (que es lo que son las estructuras ) se pueden usar de manera eficiente incluso cuando son enormes.
supercat
2
Puede ser que F # no juegue bien con la idea de pasar estructuras ref, o puede que no le guste el hecho de que las llamadas "estructuras inmutables" no lo sean, especialmente cuando están en caja. Es una lástima .net nunca implementó el concepto de pasar parámetros por un ejecutable const ref, ya que en muchos casos esa semántica es lo que realmente se requiere.
supercat
1
Por cierto, considero que el costo amortizado de GC es parte del costo de asignación de objetos; Si un GC L0 fuera necesario después de cada megabyte de asignaciones, entonces el costo de asignar 64 bytes es aproximadamente 1 / 16,000 del costo de un GC L0, más una fracción del costo de cualquier GC L1 o L2 que se vuelva necesario como consecuencia de ello.
supercat
4
"Creo que el producto de la probabilidad de ocurrencia multiplicado por el daño potencial sería mucho mayor con estructuras de lo que es actualmente con clases". FWIW, rara vez he visto tuplas de tuplas en la naturaleza y las considero un defecto de diseño, pero muy a menudo veo a las personas luchar con un rendimiento terrible cuando usan tuplas (ref) como claves en a Dictionary, por ejemplo, aquí: stackoverflow.com/questions/5850243 /…
JD
3
@Jon Han pasado dos años desde que escribí esta respuesta, y ahora estoy de acuerdo contigo en que sería preferible que al menos 2 y 3 tuplas fueran estructuras. A este respecto, se ha hecho una sugerencia de voz de usuario en el idioma F # . El problema tiene cierta urgencia, ya que ha habido un crecimiento masivo de aplicaciones en big data, finanzas cuantitativas y juegos en los últimos años.
Marc Sigrist
4

Para 2 tuplas, aún puede usar KeyValuePair <TKey, TValue> de versiones anteriores del Common Type System. Es un tipo de valor.

Una aclaración menor al artículo de Matt Ellis sería que la diferencia en la semántica de uso entre los tipos de referencia y de valor es solo "leve" cuando la inmutabilidad está en efecto (que, por supuesto, sería el caso aquí). Sin embargo, creo que habría sido mejor en el diseño de BCL no introducir la confusión de tener Tuple cruzando a un tipo de referencia en algún umbral.

Glenn Slayden
fuente
Si un valor se usará una vez después de su devolución, una estructura de campo expuesto de cualquier tamaño superará a cualquier otro tipo, siempre que no sea tan monstruosamente grande como para volar la pila. El costo de construir un objeto de clase solo se recuperará si la referencia termina compartiéndose varias veces. Hay ocasiones en las que es útil que un tipo heterogéneo de tamaño fijo de propósito general sea una clase, pero hay otras ocasiones en las que una estructura sería mejor, incluso para cosas "grandes".
supercat
Gracias por agregar esta útil regla de oro. Sin embargo, espero que no haya malinterpretado mi posición: soy un adicto a los valores. ( stackoverflow.com/a/14277068 no debe dejar dudas).
Glenn Slayden
Los tipos de valor son una de las grandes características de .net, pero desafortunadamente la persona que redactó el msdn dox no reconoció que existen múltiples casos de uso inconexos para ellos y que los diferentes casos de uso deberían tener pautas diferentes. El estilo de struct que msdn recomienda solo debe usarse con estructuras que representan un valor homogéneo, pero si uno necesita representar algunos valores independientes unidos con cinta adhesiva, no debe usar ese estilo de estructura, debe usar una estructura con Campos públicos expuestos.
supercat
0

No lo sé, pero si alguna vez has usado F #, las tuplas son parte del lenguaje. Si hice un .dll y devolví un tipo de Tuples, sería bueno tener un tipo para poner eso. Sospecho que ahora F # es parte del lenguaje (.Net 4) se hicieron algunas modificaciones a CLR para acomodar algunas estructuras comunes en F #

De http://en.wikibooks.org/wiki/F_Sharp_Programming/Tuples_and_Records

let scalarMultiply (s : float) (a, b, c) = (a * s, b * s, c * s);;

val scalarMultiply : float -> float * float * float -> float * float * float

scalarMultiply 5.0 (6.0, 10.0, 20.0);;
val it : float * float * float = (30.0, 50.0, 100.0)
Cyborg biónico
fuente