Recientemente he pasado bastantes horas optimizando código para Windows Phone 7, y una de las primeras cosas que hay que tener en mente es que el CLR(Common Language Runtime) de WP7 no es el de Windows. Incluso en la última versión (Mango) en la que han hecho grandes mejoras, como por ejemplo que ahora el GC (Garbage Collector) ofrece un rendimiento más decente con 2 generaciones, o como al aprovechamiento de las instrucciones SIMD.
Otro factor a tener en cuenta para todos esos desarrolladores que se están acercando a esta plataforma para desarrollar juegos con XNA, es que a diferencia de la mayoría de APIs que existen en .NET, en XNA se aprovechan mucho las struct para aquellos tipos con tiempo de vida relativamente bajo, algo muy común en el desarrollo de videojuegos. Por ejemplo Vector3, Vector2, Color, Matrix, etc son tipos muy usados y se suelen crear y destruir estructuras de estos tipos muy rápidamente.
Si estos tipos estuviesen definidos como class, se perdería muchísimo tiempo en reservar espacio en el Heap para luego destruirlo, por este motivo se definen como struct que permite una creación y destrucción rápida cuando se almacenan en el Stack.
Podemos llegar a tener grandes problemas de rendimiento si no comprendemos bien cómo se comportan los struct en el .NET Framework. A diferencia de las instancias de class, las instancias de struct se suelen copiar cuando las pasamos como parámetros en métodos, y es importante saber detectar todos los escenarios en los que esto ocurre.
Métodos:
Cuando hacemos llamadas como esta:
Matrix a, b, c;c = Math.Multiply( a, b );Debemos saber que Matrix está definida como struct y el paso por parámetro de estas en los métodos se hace por valor (por defecto). Dentro del cuerpo del método Multiply no se tiene la referencia a las matrices a y b, si no que dentro se trabaja con una copia de ellas, es decir, se crean dos nuevas matrices y se rellenan con los 16 (4*4) valores de las matrices fuentes. Además, la matriz que devuelve el método también se devuelve como copia, si hacemos un cálculo de cuánta memoria se ha tenido que reservar para hacer esto:
4 bytes / float
16 floats / Matrix
3 copias
3 * 16 * 4 = 192 Bytes
Esto puede llegar a ser un problema, por la basura generada, la cual pueda acelerar una pasada del GC y por el coste de CPU necesario para hacer las copias.
Properties:
Las propiedades no suelen ser muy recomendables para combinarlas con las struct, ya que su set y get tienen un comportamiento parecido al de los métodos y las estructuras se van a pasar por valor (copia).
Por ejemplo:
//Matrices en propiedades
public Matrix World {get; set;}
public Matrix View {get; set;}
public Matrix Projection {get; set;}
//Vectores como propiedad
public Vector3 Position {get; set;}
Es común usar estas propiedades como si fuesen campos, sin tener en cuenta que cada vez que se accede a ellas internamente es como si se ejecutara un “método get” que devuelve una copia de la struct:
Vector3: 3 elementos * 4 bytes / float = 12 bytes
Matrix: 16 elementos * 4 bytes / float = 64 bytes
Este comportamiento también es algo que conociéndolo se puede aprovechar. Por ejemplo, Microsoft en su API para Matrix ha colocado un atributo estático llamada identity. De forma que cada vez que queremos inicializar una matriz con la matriz identidad hacemos:
Matrix mat1 = Matrix.Identity;mat1.M11 = 0;Matrix mat2 = Matrix.Identity;
Cada vez que llamamos a Matrix.Identity nos devuelve una copia de la matriz identidad almacenada dentro de la clase Matrix de forma estática. Si no fuese así y lo que nos devolviera fuese una referencia, con la segunda línea estaríamos modificando la matriz identidad y al asignársela a mat2 esta ya no representaría a la matriz identidad.
La API de XNA está llena de esto, ejemplos:
Color.Red, Color.White, Color.Black
Vector3.Up, Vector3.Zero
Operadores:
Los operadores internamente también tienen un comportamiento similar y sufrimos copias para el paso de parámetros cuando usamos struct.
Por ejemplo:
//Operador multiplicación
Matrix worldViewProj = world * view * proj;
Internamente es como si estuviésemos haciendo llamadas a un método multiplicación al cual se le pasan por parámetro las matrices de dos en dos. Esto quiere decir que se ejecutaría una llamada al método pasándole por parámetro (copia) las matrices world y view, después el resultado se devolvería también como copia, y este se volvería a pasar al método multiplicación por valor junto con la matrix proj, y el resultado se devolverá por valor también, en total:
64 bytes / Matrix * 6 copias = 384 bytes
Si añadimos que las matrices fueron definidas como propiedades tenemos que añadir una copia más por cada acceso a matrix:
3 accesos para world, view, proj, y otra copia para realizar el set y worldviewproj es propiedad
4 copias * 64 bytes / Matrix = 256 bytes
256 bytes + 384 bytes = 640 bytes
¿Qué podemos hacer para evitar todo esto?
Métodos:
Usar los métodos en los que podamos pasar los parámetros de tipo struct como referencias:
Matrix a, b, c;//Paso de matrices por referencia (no se copia la estructura)
Matrix.Multiply(ref a, ref b, out c);Propiedades:
Usar preferiblemente campos para almacenar las struct, por ejemplo las matrices World, View, Projection de la típica clase cámara, es preferible definirlas como campos públicos ya que serán consumidas desde cada objeto que se vaya a dibujar en pantalla, ahorrándonos las copias de cada acceso.
Operadores:
Sustituir los operadores por las llamadas a métodos que permiten el paso por ref de los parámetros.
//Matrix worldViewProj = world * view * projection;
Matrix worldViewProj, temp;Matrix.Multiply(ref world, ref view, out temp);Matrix.Multiply(ref temp, ref projection, out worldViewProj);
(Importante: el código aplicando estas optimizaciones suele ser menos elegante y entendible, por lo que es recomendable solo para aquellas partes “críticas” de la aplicación)
Vamos con una segunda retahíla de enlaces relacionados con Windows Phone 7.5 (Mango). En este caso y