WP7 Problemas de rendimiento con Struct (I / II)

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)

Publicado por

jcanton

Javier is a Computer Science Engineer who has always had a passion for 3D graphics and software architecture. He learned C# almost at the same time as he learned to talk, and his first word was "base". He enjoys imparting talks about technology and has contributed in many important software and video game events. He has participated in multitude of software projects involving multitouch technologies, innovative user interfaces, augmented reality, and video games. Some of these projects were developed for companies such as Microsoft, Syderis, and nVidia. His professional achievements include being MVP for Windows DirectX and DirectX XNA for the last eight years, Xbox Ambassador, as well as Microsoft Student Partner and Microsoft Most Valuable Student during his years at college. Currently he works at Plainconcepts and he is the development team lead at WaveEngine project.

Un comentario sobre “WP7 Problemas de rendimiento con Struct (I / II)”

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *