c# - studio - ¿Cuándo se estructura la respuesta?
visual studio community (12)
Estoy haciendo un proyecto de hobby Raytracer, y originalmente estaba usando estructuras para mis objetos Vector y Ray, y pensé que un raytracer era la situación perfecta para usarlos: creas millones de ellos, no viven más que un solo método, son livianos. Sin embargo, simplemente cambiando ''struct'' a ''clase'' en Vector y Ray, obtuve una ganancia de rendimiento muy significativa.
¿Lo que da? Ambos son pequeños (3 flotadores para vectores, 2 vectores para un rayo), no se copien excesivamente. Los paso a los métodos cuando sea necesario, por supuesto, pero eso es inevitable. Entonces, ¿cuáles son las trampas comunes que matan el rendimiento cuando se usan estructuras? Leí this artículo de MSDN que dice lo siguiente:
Cuando ejecutas este ejemplo, verás que el ciclo struct es de orden de magnitud más rápido. Sin embargo, es importante tener cuidado con el uso de ValueTypes cuando los trata como objetos. Esto agrega una sobrecarga extra de boxeo y desempaquetado a su programa, ¡y puede terminar costándole más de lo que le costaría si se hubiera quedado con objetos! Para ver esto en acción, modifique el código anterior para usar una matriz de foos y barras. Descubrirá que el rendimiento es más o menos igual.
Sin embargo, es bastante antiguo (2001) y todo el "ponerlos en una matriz causa que el boxeo / unboxing" me parezca extraño. ¿Es eso cierto? Sin embargo, precalculé los rayos primarios y los puse en una matriz, así que tomé este artículo y calculé el rayo primario cuando lo necesitaba y nunca los agregué a una matriz, pero no cambió nada: con clases, todavía era 1.5 veces más rápido.
Estoy ejecutando .NET 3.5 SP1, que creo que solucionó un problema en el que los métodos struct nunca se alineaban, por lo que tampoco puede ser.
Entonces, básicamente: ¿algún consejo, cosas que considerar y qué evitar?
EDITAR: Como se sugirió en algunas respuestas, configuré un proyecto de prueba donde intenté pasar las estructuras como ref. Los métodos para agregar dos vectores:
public static VectorStruct Add(VectorStruct v1, VectorStruct v2)
{
return new VectorStruct(v1.X + v2.X, v1.Y + v2.Y, v1.Z + v2.Z);
}
public static VectorStruct Add(ref VectorStruct v1, ref VectorStruct v2)
{
return new VectorStruct(v1.X + v2.X, v1.Y + v2.Y, v1.Z + v2.Z);
}
public static void Add(ref VectorStruct v1, ref VectorStruct v2, out VectorStruct v3)
{
v3 = new VectorStruct(v1.X + v2.X, v1.Y + v2.Y, v1.Z + v2.Z);
}
Para cada uno obtuve una variación del siguiente método de referencia:
VectorStruct StructTest()
{
Stopwatch sw = new Stopwatch();
sw.Start();
var v2 = new VectorStruct(0, 0, 0);
for (int i = 0; i < 100000000; i++)
{
var v0 = new VectorStruct(i, i, i);
var v1 = new VectorStruct(i, i, i);
v2 = VectorStruct.Add(ref v0, ref v1);
}
sw.Stop();
Console.WriteLine(sw.Elapsed.ToString());
return v2; // To make sure v2 doesn''t get optimized away because it''s unused.
}
Todos parecen funcionar bastante idénticos. ¿Es posible que el JIT los optimice para la forma óptima de pasar esta estructura?
EDIT2: Debo señalar que el uso de estructuras en mi proyecto de prueba es aproximadamente un 50% más rápido que el uso de una clase. ¿Por qué esto es diferente para mi Raytracer? No lo sé.
¿Ha perfilado la aplicación? La creación de perfiles es la única forma segura de ver dónde está el problema de rendimiento real. Hay operaciones que generalmente son mejores / peores en las estructuras, pero a menos que tu perfil solo estarías adivinando cuál es el problema.
Básicamente, no los pongas demasiado grandes y pásalos por ref cuando puedas. Descubrí esto de la misma manera ... Al cambiar mis clases Vector y Ray a estructuras.
Con más memoria que se transmite, es probable que provoque la caché.
Creo que la clave está en estas dos afirmaciones de tu publicación:
creas millones de ellos
y
Los paso a los métodos cuando sea necesario, por supuesto
Ahora, a menos que su estructura tenga un tamaño menor o igual a 4 bytes (u 8 bytes si está en un sistema de 64 bits), está copiando mucho más en cada llamada a un método, entonces, si simplemente pasó una referencia de objeto.
En las recomendaciones sobre cuándo usar una estructura, dice que no debe ser mayor que 16 bytes. Su Vector tiene 12 bytes, que está cerca del límite. El Ray tiene dos vectores, poniéndolo a 24 bytes, que está claramente por encima del límite recomendado.
Cuando una estructura supera los 16 bytes, ya no se puede copiar de manera eficiente con un solo conjunto de instrucciones, sino que se utiliza un ciclo. Por lo tanto, al pasar este límite "mágico", en realidad está haciendo mucho más trabajo cuando pasa una estructura que cuando pasa una referencia a un objeto. Esta es la razón por la cual el código es más rápido con las clases aunque hay más sobrecarga al asignar los objetos.
El Vector todavía podría ser una estructura, pero el Rayo es simplemente demasiado grande para funcionar bien como una estructura.
Lo primero que buscaría es asegurarme de que haya implementado explícitamente Equals y GetHashCode. No hacerlo significa que la implementación en tiempo de ejecución de cada uno de estos hace algunas operaciones muy costosas para comparar dos instancias de estructura (internamente utiliza la reflexión para determinar cada uno de los campos privados y luego los evalúa para la igualdad, esto causa una cantidad significativa de asignación) .
En general, sin embargo, lo mejor que puede hacer es ejecutar su código en un generador de perfiles y ver dónde están las partes lentas. Puede ser una experiencia reveladora.
Mi propio rastreador de rayos también usa struct Vectors (aunque no Rayos) y cambiar Vector por clase no parece tener ningún impacto en el rendimiento. Actualmente estoy usando tres dobles para el vector, por lo que podría ser más grande de lo que debería ser. Una cosa a tener en cuenta, y esto podría ser obvio, pero no fue para mí, y eso es ejecutar el programa fuera del estudio visual. Incluso si lo configura para la generación optimizada de versiones, puede obtener un impulso de velocidad masivo si inicia el exe fuera de VS. Cualquier evaluación comparativa que haga debe tomar esto en consideración.
Si bien la funcionalidad es similar, las estructuras suelen ser más eficientes que las clases. Debe definir una estructura, en lugar de una clase, si el tipo funcionará mejor como un tipo de valor que un tipo de referencia.
Específicamente, los tipos de estructura deberían cumplir con todos estos criterios:
- Lógicamente representa un valor único
- Tiene un tamaño de instancia de menos de 16 bytes
- No se cambiará después de la creación
- No se lanzará a un tipo de referencia
Si las estructuras son pequeñas, y no existen demasiadas a la vez, DEBERÍA colocarlas en la pila (siempre que sea una variable local y no un miembro de una clase) y no en el montón, esto significa que el GC no lo hace Debe invocarse y la asignación / desasignación de memoria debe ser casi instantánea.
Al pasar una estructura como parámetro para funcionar, la estructura se copia, lo que no solo significa más asignaciones / desasignaciones (de la pila, que es casi instantánea, pero aún tiene sobrecarga), sino la sobrecarga en la transferencia de datos entre las 2 copias . Si pasa por referencia, esto no es un problema, ya que solo le indica dónde leer los datos, en lugar de copiarlos.
No estoy 100% seguro de esto, pero sospecho que devolver las matrices a través de un parámetro de "salida" también puede darle un impulso de velocidad, ya que la memoria en la pila está reservada para ella y no necesita copiarse como la pila se "desenrolla" al final de las llamadas a funciones.
También puede hacer estructuras en objetos Nullable. Las clases personalizadas no podrán crearse
como
Nullable<MyCustomClass> xxx = new Nullable<MyCustomClass>
donde con una estructura es anulable
Nullable<MyCustomStruct> xxx = new Nullable<MyCustomStruct>
Pero perderá (obviamente) todas sus características de herencia
Todo lo que se escriba sobre el boxeo / desempaquetado previo a los genéricos .NET se puede tomar con algo de sal. Los tipos de colecciones genéricas han eliminado la necesidad de boxeo y desembalaje de tipos de valores, lo que hace que el uso de estructuras en estas situaciones sea más valioso.
En cuanto a su ralentización específica, probablemente necesitemos ver algún código.
Una matriz de estructuras sería una sola estructura contigua en la memoria, mientras que los elementos de una matriz de objetos (instancias de tipos de referencia) deben abordarse individualmente mediante un puntero (es decir, una referencia a un objeto en el montón recogido de basura). Por lo tanto, si aborda colecciones grandes de elementos a la vez, las estructuras le darán un aumento de rendimiento ya que necesitan menos indirecciones. Además, las estructuras no se pueden heredar, lo que podría permitir al compilador realizar optimizaciones adicionales (pero eso es solo una posibilidad y depende del compilador).
Sin embargo, las estructuras tienen una semántica de asignación bastante diferente y tampoco pueden heredarse. Por lo tanto, normalmente evitaría las estructuras, excepto por los motivos de rendimiento dados cuando sea necesario.
estructura
Una matriz de valores v codificados por una estructura (tipo de valor) se ve así en la memoria:
vvvv
clase
Una matriz de valores v codificados por una clase (tipo de referencia) se ve así:
pppp
..v..v ... vv.
donde p son estos punteros, o referencias, que apuntan a los valores reales v en el montón. Los puntos indican otros objetos que pueden estar intercalados en el montón. En el caso de los tipos de referencia, debe hacer referencia a v a través de la p correspondiente, en el caso de los tipos de valores, puede obtener el valor directamente a través de su desplazamiento en la matriz.
Yo uso estructuras básicamente para objetos de parámetros, devolviendo múltiples datos de una función, y ... nada más. No sé si es "correcto" o "incorrecto", pero eso es lo que hago.