3 de Julio, 2020

Punteros a campos de estructuras

Esta es otra técnica orientada a mejorar el código generado por el compilador (especialmente SDCC), cuando trabajamos con micros de 8-bits como por el ejemplo el Z80.

Usar punteros a campos de una estructura entra quizás en la categoría de usar variables globales, porque se trata de utilizar un método de acceso a variables en C que mejor se traduce a ensamblador de Z80.

El caso de uso es el descrito cuando expliqué mis listas en 8-bits:

mientras el juego no acabe:
   leer entrada teclado/joystick
   actualizar entidades
   dibujar entidades
   esperar si es necesario, para velocidad constante

Cuando recorremos la lista y llamamos por ejemplo a la función de actualizar estado para cada entidad, esa entidad tendrá acceso a sus datos usando un puntero a una estructura, que tendrá un número de campos.

Veamos cómo se acceden a esos campos con el siguiente ejemplo.

#include<stdint.h>

struct st_entity
{
    uint8_t type;
    uint8_t x;
    uint8_t y;
    void (*update)();
    void (*draw)();
    struct st_entity *n;
};

struct st_entity *it;

void update()
{
    it->x += 2;
    it->y++;
}

Como ejemplo no es muy significativo porque SDCC no va a generar muy mal código en una función tan pequeña, pero al menos podremos ver de una forma empírica cómo se accede a los campos de la estructura.

El código generado es:

_update::
;test2.c:17: it->x += 2;
	ld	hl, (_it)
	inc	hl
	ld	a, (hl)
	add	a, #0x02
	ld	(hl), a
;test2.c:18: it->y++;
	ld	hl, (_it)
	inc	hl
	inc	hl
	inc	(hl)
;;test2.c:19: }
	ret

El compilador carga en hl la posición de memoria guardada en el puntero a la estructura, y luego incrementa ese registro hasta llegar a la posición de memoria en la estructura donde están los datos que necesitamos.

Ya vemos como ha cargado el puntero cada vez, y se ha desplazado al campo. En estructuras más grandes y con más código, el compilador generará código que es mucho peor (probablemente usando más registros, incluso ix).

Una vez expuesto el problema, veamos una posible solución: usar punteros a los campos de la estructura.

En el código que llama a la función update prepararemos esos punteros antes de hacer la llamada y, aunque seguiremos teniendo el mismo problema en ese punto, el código generado en las funciones de actualización de todas nuestras entidades será más corto y más rápido.

El código que prepara la llamada sería algo como:

// variables globales
uint8_t *it_x, *it_y;

// antes de llamar a update
it_x = &it->x;
it_y = &it->y;

De esta forma la función que actualiza el estado de esta entidad podrá acceder a los campos x e y directamente usando *it_x e *it_y (que en C significa "opera con el valor apuntado por el puntero").

void update()
{
    *it_x += 2;
    (*it_y)++;
}

Aún en nuestro ejemplo tan simple se puede ver la mejora:

_update::
;test2.c:25: *it_x += 2;
	ld	hl, (_it_x)
	ld	a, (hl)
	add	a, #0x02
	ld	(hl), a
;test2.c:26: (*it_y)++;
	ld	hl, (_it_y)
	inc	(hl)
;test2.c:27: }
	ret

Mucho mejor porque evitamos tener incrementar hl para llegar al campo de la estructura porque estamos accediendo a él directamente con un puntero. Además vemos que algunas operaciones simples se pueden hacer directamente sobre hl en menos pasos (como incrementar una variable de 8-bits).

Esta técnica es probablemente la que más rendimiento me ha permitido conseguir en muchos de mis juegos para Z80, aparte de ayudar al compilador a conseguir código más compacto y ahorrar memoria (que muchas veces es muy escasa en estas máquinas).

La única pega quizás es la notación en C que es un poco incómoda, pero es muy fácil acostumbrarse y los beneficios bien merecen la pena ;).

Anotación por Juan J. Martínez, clasificada en: c, programming, gamedev.

Hay 3 comentarios

Gravatar

Muy curioso.

En principio parece que solo estás moviendo el código de inicialización fuera de la función update y no esperaría una mejora pero el nuevo código de inicialización es mas “compacto” y sdcc lo optimiza mejor que si aparece “desperdigado” dentro de update.

por un visitante, en 2020-07-03 11:18:39

Gravatar

Además estos ejemplos son muy básicos. En una función “real” tendrás más accesos a los campos, y es posible que las operaciones para acceder a esa posición de memoria se repitan varias veces (y ahí es donde SDCC dejará de generar código eficiente).

Con los punteros sabes que siempre va a ser lo mismo: cargar el puntero y hacer la operación directamente; y te saltas el desplazamiento desde el comienzo de la estructura.

por Juanjo, en 2020-07-03 14:20:49

Gravatar

CppCon 2016: Jason Turner “Rich Code for Tiny Computers: A Simple Commodore 64 Game in C++17”

https://www.youtube.com/watch?v=zBkNBP00wJE

poco que ver con sdcc pero me pareció interesante y quería compartirlo

por un visitante, en 2020-08-01 21:27:02

Los comentarios están cerrados: los comentarios se cierran automáticamente una vez pasados 30 días. Si quieres comentar algo acerca de la anotación, puedes hacerlo por e-mail.

Algunas anotaciones relacionadas: