Estructuras versus clases

93

Estoy a punto de crear 100.000 objetos en código. Son pequeños, solo con 2 o 3 propiedades. Los pondré en una lista genérica y cuando estén, los haré un bucle y comprobaré el valor ay tal vez actualizaré el valor b.

¿Es más rápido / mejor crear estos objetos como clase o como estructura?

EDITAR

a. Las propiedades son tipos de valor (excepto la cadena, ¿creo?)

si. Es posible que (aún no estamos seguros) tengan un método de validación

EDITAR 2

Me preguntaba: ¿los objetos del montón y la pila son procesados ​​por igual por el recolector de basura, o eso funciona de manera diferente?

Michel
fuente
2
¿Van a tener solo campos públicos o también van a tener métodos? ¿Son los tipos tipos primitivos, como enteros? ¿Estarán contenidos en una matriz o en algo como List <T>?
JeffFerguson
14
¿Una lista de estructuras mutables? Cuidado con el velociraptor.
Anthony Pegram
1
@Anthony: me temo que me estoy perdiendo la broma del velociraptor: -s
Michel
5
La broma del velociraptor es de XKCD. Pero cuando está lanzando el detalle de implementación / idea errónea de 'los tipos de valor se asignan en la pila' (elimine según corresponda), entonces es Eric Lippert con quien debe tener cuidado ...
Greg Beech
4
velociraptor: imgs.xkcd.com/comics/goto.png
WernerCD

Respuestas:

137

Lo es más rápido crear estos objetos como clase o como estructura?

Eres la única persona que puede determinar la respuesta a esa pregunta. Pruébalo en ambos sentidos mida una métrica de rendimiento relevante, significativa y centrada en el usuario, y luego sabrá si el cambio tiene un efecto significativo en los usuarios reales en escenarios relevantes.

Las estructuras consumen menos memoria de pila (porque son más pequeñas y se compactan más fácilmente, no porque estén "en la pila"). Pero tardan más en copiarse que una copia de referencia. No sé cuáles son sus métricas de rendimiento para el uso de memoria o la velocidad; aquí hay una compensación y usted es la persona que sabe cuál es.

Es mejor crear estos objetos como clase o como estructura?

Tal vez clase, tal vez estructura. Como regla general: si el objeto es:
1. Pequeño
2. Lógicamente un valor inmutable
3. Hay muchos de ellos
Entonces consideraría convertirlo en una estructura. De lo contrario, me quedaría con un tipo de referencia.

Si necesita mutar algún campo de una estructura, generalmente es mejor construir un constructor que devuelva una estructura completamente nueva con el campo configurado correctamente. Eso es quizás un poco más lento (¡mídelo!), Pero lógicamente es mucho más fácil razonar.

¿El recolector de basura procesa los objetos del montón y de la pila por igual?

No , no son iguales porque los objetos de la pila son las raíces de la colección . El recolector de basura no necesita preguntarse nunca "¿esta cosa en la pila está viva?" porque la respuesta a esa pregunta es siempre "Sí, está en la pila". (Ahora, no puedes confiar en eso para mantener vivo un objeto porque la pila es un detalle de implementación. El jitter puede introducir optimizaciones que, digamos, registran lo que normalmente sería un valor de pila, y luego nunca está en la pila por lo que el GC no sabe que todavía está vivo. Un objeto registrado puede tener sus descendientes recolectados agresivamente, tan pronto como el registro que lo sostiene no se vuelva a leer).

Pero el recolector de basura no tiene que tratar los objetos en la pila como vivos, de la misma manera que trata a cualquier objeto que se sabe que vivo como vivo. El objeto de la pila puede referirse a objetos asignados al montón que deben mantenerse activos, por lo que el GC debe tratar los objetos de la pila como objetos asignados al montón vivos para determinar el conjunto activo. Pero, obviamente, no se tratan como "objetos vivos" a los efectos de compactar el montón, porque en primer lugar no están en el montón.

¿Está claro?

Eric Lippert
fuente
Eric, ¿sabes si el compilador o el jitter hacen uso de la inmutabilidad (quizás si se aplica con ella readonly) para permitir optimizaciones? No dejaría que eso afecte una elección sobre la mutabilidad (en teoría, soy un loco por los detalles de eficiencia, pero en la práctica, mi primer paso hacia la eficiencia siempre es intentar tener una garantía de corrección tan simple como pueda y, por lo tanto, no tener que hacerlo. desperdiciar ciclos de CPU y ciclos cerebrales en controles y casos extremos, y ser adecuadamente mutable o inmutable ayuda allí), pero contrarrestaría cualquier reacción instintiva a su diciendo que la inmutabilidad puede ser más lenta.
Jon Hanna
@Jon: el compilador de C # optimiza los datos constantes pero no los de solo lectura . No sé si el compilador jit realiza alguna optimización de almacenamiento en caché en campos de solo lectura.
Eric Lippert
Una lástima, como sé, el conocimiento de la inmutabilidad permite algunas optimizaciones, pero en ese momento alcanzo los límites de mi conocimiento teórico, pero son límites que me encantaría extender. Mientras tanto, "puede ser más rápido en ambos sentidos, he aquí por qué, ahora prueba y descubre cuál se aplica en este caso" es útil poder decir :)
Jon Hanna
Recomendaría leer simple-talk.com/dotnet/.net-framework/… y su propio artículo (@Eric): blogs.msdn.com/b/ericlippert/archive/2010/09/30/… para comenzar a bucear en detalles. Hay muchos otros buenos artículos alrededor. Por cierto, la diferencia en el procesamiento de 100 000 pequeños objetos en la memoria es apenas perceptible debido a que existe una sobrecarga de memoria (~ 2,3 MB) para la clase. Puede comprobarse fácilmente mediante una simple prueba.
Nick Martyshchenko
Sí, está claro. Muchas gracias por tu respuesta completa (¿extensa es mejor? Google translate dio 2 traducciones. Quería decir que te tomaste el tiempo para no escribir una respuesta corta, pero también te tomaste el tiempo para escribir todos los detalles).
Michel
23

A veces con struct no es necesario llamar al constructor new () y asignar directamente los campos, lo que lo hace mucho más rápido de lo habitual.

Ejemplo:

Value[] list = new Value[N];
for (int i = 0; i < N; i++)
{
    list[i].id = i;
    list[i].isValid = true;
}

es aproximadamente de 2 a 3 veces más rápido que

Value[] list = new Value[N];
for (int i = 0; i < N; i++)
{
    list[i] = new Value(i, true);
}

donde Valuees un structcon dos campos ( idy isValid).

struct Value
{
    int id;
    bool isValid;

    public Value(int i, bool isValid)
    {
        this.i = i;
        this.isValid = isValid;
    }
}

Por otro lado, es necesario mover los elementos o seleccionar tipos de valores, todo lo que copiar lo ralentizará. Para obtener la respuesta exacta, sospecho que tienes que perfilar tu código y probarlo.

John Alexiou
fuente
Obviamente, las cosas se vuelven mucho más rápidas cuando clasifica los valores sobre los límites nativos también.
leppie
Sugeriría usar un nombre que no sea list, dado que el código indicado no funcionará con un List<Value>.
supercat
7

Las estructuras pueden parecer similares a las clases, pero hay diferencias importantes que debe conocer. En primer lugar, las clases son tipos de referencia y las estructuras son tipos de valor. Mediante el uso de estructuras, puede crear objetos que se comporten como los tipos integrados y también disfrutar de sus beneficios.

Cuando llame al operador New en una clase, se asignará en el montón. Sin embargo, cuando crea una instancia de una estructura, se crea en la pila. Esto producirá mejoras en el rendimiento. Además, no tratará con referencias a una instancia de una estructura como lo haría con las clases. Trabajará directamente con la instancia de estructura. Debido a esto, cuando se pasa una estructura a un método, se pasa por valor en lugar de como referencia.

Más aquí:

http://msdn.microsoft.com/en-us/library/aa288471(VS.71).aspx

kyndigs
fuente
4
Sé que lo dice en MSDN, pero MSDN no cuenta toda la historia. Stack vs. heap es un detalle de implementación y las estructuras no siempre van en la pila. Para ver solo un blog reciente sobre esto, consulte: blogs.msdn.com/b/ericlippert/archive/2010/09/30/…
Anthony Pegram
"... se pasa por valor ..." tanto las referencias como las estructuras se pasan por valor (a menos que se use 'ref') - lo que difiere es si se pasa un valor o una referencia, es decir, las estructuras se pasan valor por valor , los objetos de clase se pasan referencia por valor y los parámetros marcados con referencia pasan referencia por referencia.
Paul Ruane
10
Ese artículo es engañoso en varios puntos clave y le he pedido al equipo de MSDN que lo revise o lo elimine.
Eric Lippert
2
@supercat: para abordar su primer punto: el punto más importante es que en el código administrado donde se almacena un valor o una referencia a un valor es en gran medida irrelevante . Hemos trabajado duro para crear un modelo de memoria que la mayoría de las veces permite a los desarrolladores permitir que el tiempo de ejecución tome decisiones de almacenamiento inteligentes en su nombre. Estas distinciones importan mucho cuando no comprenderlas tiene consecuencias estrepitosas como ocurre en C; no tanto en C #.
Eric Lippert
1
@supercat: para abordar su segundo punto, ninguna estructura mutable es en su mayoría malvada. Por ejemplo, void M () {S s = new S (); s.Blah (); N (s); }. Refactorizar para: void DoBlah (S s) {s.Blah (); } void M (S s = new S (); DoBlah (s); N (s);}. Eso acaba de introducir un error porque S es una estructura mutable. ¿Viste inmediatamente el error? ¿O el hecho de que S es ¿Una estructura mutable te oculta el error?
Eric Lippert
6

Las matrices de estructuras se representan en el montón en un bloque de memoria contiguo, mientras que una matriz de objetos se representa como un bloque contiguo de referencias con los objetos reales en otra parte del montón, lo que requiere memoria tanto para los objetos como para sus referencias de matriz. .

En este caso, como los está colocando en un List<>(y un List<>está respaldado en una matriz), sería más eficiente, en términos de memoria, usar estructuras.

(Sin embargo, tenga en cuenta que las matrices grandes encontrarán su camino en el Montón de objetos grandes donde, si su vida útil es larga, pueden tener un efecto adverso en la gestión de la memoria de su proceso. Recuerde, también, que la memoria no es la única consideración).

Paul Ruane
fuente
Puedes usar refpalabras clave para lidiar con esto.
leppie
"Sin embargo, tenga en cuenta que las matrices grandes encontrarán su camino en la pila de objetos grandes donde, si su vida útil es larga, pueden tener un efecto adverso en la gestión de la memoria de su proceso". - No estoy muy seguro de por qué piensas eso. Estar asignado en el LOH no causará ningún efecto adverso en la administración de la memoria a menos que (posiblemente) sea un objeto de corta duración y desee recuperar la memoria rápidamente sin esperar una colección Gen 2.
Jon Artus
@Jon Artus: la LOH no se compacta. Cualquier objeto de larga duración dividirá el LOH en el área de memoria libre antes y el área después. Se requiere memoria contigua para la asignación y si estas áreas no son lo suficientemente grandes para una asignación, se asigna más memoria al LOH (es decir, obtendrá la fragmentación del LOH).
Paul Ruane
4

Si tienen semántica de valor, probablemente debería usar una estructura. Si tienen semántica de referencia, probablemente debería usar una clase. Hay excepciones, que en su mayoría se inclinan hacia la creación de una clase incluso cuando hay semántica de valores, pero parten de ahí.

En cuanto a su segunda edición, el GC solo se ocupa del montón, pero hay mucho más espacio en el montón que en la pila, por lo que poner cosas en la pila no siempre es una victoria. Además de lo cual, una lista de tipos de estructura y una lista de tipos de clase estarán en el montón de cualquier manera, por lo que esto es irrelevante en este caso.

Editar:

Estoy empezando a considerar que el término maldad es dañino. Después de todo, hacer que una clase sea mutable es una mala idea si no se necesita activamente, y no descartaría usar una estructura mutable. Sin embargo, es una mala idea tan a menudo que casi siempre es una mala idea, pero en su mayoría simplemente no coincide con la semántica de valores, por lo que no tiene sentido usar una estructura en el caso dado.

Puede haber excepciones razonables con estructuras anidadas privadas, donde todos los usos de esa estructura están, por lo tanto, restringidos a un ámbito muy limitado. Sin embargo, esto no se aplica aquí.

Realmente, creo que "muta, por lo que es un mal estudio" no es mucho mejor que hablar del montón y la pila (que al menos tiene algún impacto en el rendimiento, incluso si se tergiversa con frecuencia). "Muta, por lo que es muy probable que no tenga sentido considerar que tiene una semántica de valor, por lo que es una estructura mala" es solo un poco diferente, pero creo que es importante.

Jon Hanna
fuente
3

La mejor solución es medir, volver a medir y luego medir un poco más. Puede haber detalles de lo que está haciendo que pueden dificultar una respuesta simplificada y fácil como "usar estructuras" o "usar clases".

FMM
fuente
Estoy de acuerdo con la parte de la medida, pero en mi opinión era un ejemplo sencillo y claro, y pensé que tal vez se podrían decir algunas cosas genéricas al respecto. Y resultó que algunas personas lo hicieron.
Michel
3

Una estructura es, en el fondo, nada más ni menos que una agregación de campos. En .NET es posible que una estructura "pretenda" ser un objeto, y para cada tipo de estructura .NET define implícitamente un tipo de objeto de montón con los mismos campos y métodos que, al ser un objeto de montón, se comportarán como un objeto. . Una variable que contiene una referencia a dicho objeto de montón (estructura "en caja") exhibirá semántica de referencia, pero una que contiene una estructura directamente es simplemente una agregación de variables.

Creo que gran parte de la confusión estructura versus clase se debe al hecho de que las estructuras tienen dos casos de uso muy diferentes, que deberían tener pautas de diseño muy diferentes, pero las pautas de MS no distinguen entre ellas. A veces existe la necesidad de algo que se comporte como un objeto; en ese caso, las pautas de MS son bastante razonables, aunque el "límite de 16 bytes" probablemente debería ser más como 24-32. A veces, sin embargo, lo que se necesita es una agregación de variables. Una estructura utilizada para ese propósito debería consistir simplemente en un montón de campos públicos, y posiblemente una Equalsanulación,ToString anulación yIEquatable(itsType).Equalsimplementación. Las estructuras que se utilizan como agregaciones de campos no son objetos y no deberían pretender serlo. Desde el punto de vista de la estructura, el significado de campo debe ser ni más ni menos que "lo último que se escribe en este campo". Cualquier significado adicional debe ser determinado por el código del cliente.

Por ejemplo, si una estructura de agregación de variables tiene miembros Minimumy Maximum, la estructura en sí no debería prometer eso Minimum <= Maximum. El código que recibe dicha estructura como parámetro debe comportarse como si se le pasaran valores Minimumy separados Maximum. Un requisito que Minimumno sea mayor que Maximumdebe considerarse como un requisito de que un Minimumparámetro no sea mayor que Maximumuno pasado por separado .

Un patrón útil a considerar a veces es tener una ExposedHolder<T>clase definida como:

class ExposedHolder<T>
{
  public T Value;
  ExposedHolder() { }
  ExposedHolder(T val) { Value = T; }
}

Si uno tiene List<ExposedHolder<someStruct>>, where someStructes una estructura de agregación de variables, uno puede hacer cosas como myList[3].Value.someField += 7;, pero darle myList[3].Valuea otro código le dará el contenido en Valuelugar de darle un medio para alterarlo. Por el contrario, si se usara List<someStruct>, sería necesario usar var temp=myList[3]; temp.someField += 7; myList[3] = temp;. Si uno usa un tipo de clase mutable, exponer el contenido de myList[3]un código externo requeriría copiar todos los campos a algún otro objeto. Si uno usara un tipo de clase inmutable, o una estructura de "estilo de objeto", sería necesario construir una nueva instancia que fuera como myList[3]excepto someFieldcuál era diferente, y luego almacenar esa nueva instancia en la lista.

Una nota adicional: si está almacenando una gran cantidad de cosas similares, puede ser bueno almacenarlas en matrices de estructuras posiblemente anidadas, preferiblemente tratando de mantener el tamaño de cada matriz entre 1K y 64K más o menos. Los arreglos de estructuras son especiales, en el sentido de que indexar uno producirá una referencia directa a una estructura interna, por lo que se puede decir "a [12] .x = 5;". Aunque se pueden definir objetos similares a matrices, C # no les permite compartir dicha sintaxis con matrices.

Super gato
fuente
1

Usa clases.

En una nota general. ¿Por qué no actualizar el valor b a medida que los crea?

Preet Sangha
fuente
1

Desde una perspectiva de C ++, estoy de acuerdo en que será más lento modificar las propiedades de una estructura en comparación con una clase. Pero sí creo que será más rápido leerlos debido a que la estructura se asigna en la pila en lugar del montón. Leer datos del montón requiere más comprobaciones que de la pila.

Robert
fuente
1

Bueno, si va con struct después de todo, elimine la cadena y use un búfer de bytes o caracteres de tamaño fijo.

Eso es re: rendimiento.

Daniel Mošmondor
fuente