• Autor de la entrada:
  • Tiempo de lectura:16 minutos de lectura

Las FPU reducen el tiempo de ejecución de operaciones matemáticas, en esta entrada se cuantifican los tiempos de ejecución de las más elementales mediante un benchmark en la FPU de MicroBlazeTM. También se proponen varias optimizaciones y se mide su impacto.

Cuando el tiempo de ejecución es crítico se deben explotar al máximo las posibilidades de paralelización que ofrecen las FPGA. Sin embargo, por cuestiones de espacio podría no ser posible implementar todas las operaciones a nivel RTL, en ese momento hay que recurrir a filosofías de trabajo más tradicionales que utilicen procesadores de propósito general.

Instrucciones extendidas de MicroBlaze y recursos consumidos

Al realizar la especificación de MicroBlazeTM, Xilinx® ofrece varias posibilidades a la hora de extender las instrucciones relacionadas con la FPU.

  • FPU básica
  • FPU extendida

RecursoBásicaExtendida
Área+6%+10%
DSP48E22
Recursos consumidos por MicroBlazeTM con FPU en una XC7A35T

Método del benchmark

Se medirá el tiempo empleado en realizar operaciones sobre un array de n elementos, el código será el siguiente

#include <math.h> 

int mult_bench(int n, float *a, float *b, float *c){ 
       for(int i = 0; i<n; i++){ 
             *(c+i) = *(a+i) * *(b+i); 
       } 
       return 0; 
} 

int div_bench(int n, float *a, float *b, float *c){ 
       for(int i = 0; i<n; i++){ 
             *(c+i) = *(a+i) / *(b+i); 
       } 
       return 0; 
} 

int sqrt_bench(int n, float *b, float *c){ 
       for(int i = 0; i<n; i++){ 
             *(c+i) = sqrtf(*(b+i)); 
       } 
       return 0; 
} 

int pow_bench(int n, float *a, float *b, float *c){ 
       for(int i = 0; i<n; i++){ 
             *(c+i) = powf(*(a+i), *(b+i)); 
       } 
       return 0; 
} 

Para medir el tiempo de ejecución se utiliza un timer, el diagrama de bloques en VivadoTM queda de la siguiente manera

Diagrama de bloques en Vivado para benchmark de MicroBlaze
Diagrama de bloques en VivadoTM para benchmark de MicroBlazeTM

Se realizan varias pruebas utilizando distintos ‘n’ tamaños de array, con ello se hará una media para obtener un resultado intermedio entre llamadas a la FPU más y menos intensivas. El código con el que se lanzará el benchmark en la FPU de MicroBlazeTM es el siguiente

#include <stdio.h>
#include <math.h>
#include "platform.h"
#include "xil_printf.h"
#include "xtmrctr.h"
#include "xparameters.h"
#include "benchmark.h"

#define TMR_DEVICE_ID XPAR_TMRCTR_0_DEVICE_ID
#define TIMER_COUNTER_0	 0

const int N_OPERATIONS = 200;
int begin, end, status;

XTmrCtr	TMRInst;

int printFloat(float f);
int print_abc();
int startTable(float *a, float *b, float *c);

int launchBenchmark(float *a, float *b, float *c){

       print("Mul FPU ");
       begin=XTmrCtr_GetValue(&TMRInst, TIMER_COUNTER_0);
       mult_bench(N_OPERATIONS, a, b, c);
       end=XTmrCtr_GetValue(&TMRInst, TIMER_COUNTER_0);
       //print_abc(a, b, c);
       xil_printf("ticks: %d \r\n", end-begin);

       print("Div FPU ");
       begin=XTmrCtr_GetValue(&TMRInst, TIMER_COUNTER_0);
       div_bench(N_OPERATIONS, a, b, c);
       end=XTmrCtr_GetValue(&TMRInst, TIMER_COUNTER_0);
       //print_abc(a, b, c);
       xil_printf("ticks: %d \r\n", end-begin);

       print("Sqrt FPU ");
       begin=XTmrCtr_GetValue(&TMRInst, TIMER_COUNTER_0);
       sqrt_bench(N_OPERATIONS, b, c);
       end=XTmrCtr_GetValue(&TMRInst, TIMER_COUNTER_0);
       //print_abc(a, b, c);
       xil_printf("ticks: %d \r\n", end-begin);

       print("Pow FPU ");
       begin=XTmrCtr_GetValue(&TMRInst, TIMER_COUNTER_0);
       pow_bench(N_OPERATIONS, a, b, c);
       end=XTmrCtr_GetValue(&TMRInst, TIMER_COUNTER_0);
       //print_abc(a, b, c);
       xil_printf("ticks: %d \r\n", end-begin);

       return 0;
}

int main() {
	float a[N_OPERATIONS+1];
	float b[N_OPERATIONS+1];
	float c[N_OPERATIONS+1];

    init_platform();
    XTmrCtr_Initialize(&TMRInst, TMR_DEVICE_ID);
    XTmrCtr_SetOptions(&TMRInst, TIMER_COUNTER_0, XTC_AUTO_RELOAD_OPTION);
    XTmrCtr_Start(&TMRInst, TIMER_COUNTER_0);

    startTable(&a[0], &b[0], &c[0]);
    launchBenchmark(&a[0], &b[0], &c[0]);

    cleanup_platform();
    return 0;
}

int print_abc(float *a, float *b, float *c){
	for(int i=0; i<=N_OPERATIONS; i++){
	    xil_printf("%d: ", i);
	    printFloat(a[i]);
	    printFloat(b[i]);
	    printFloat(c[i]);
	    print("\n\r");
	}
	return 0;
}

int printFloat(float f){
	int whole = f;
	int thousandths = (f - whole) * 1000;
	xil_printf("%d.%03d ", whole, thousandths);
	return 0;
}

int startTable(float *a, float *b, float *c){
	print("\n\r**Benchmark FPU**\n\r");
	xil_printf("N_op: %d\r\n", N_OPERATIONS);
	//Genera números negativos
	for(int i=0; i<=(N_OPERATIONS/2+1); i++){
	    a[i]=(float)(i*(-1));
	    b[N_OPERATIONS-i]=(float)i;
	    c[i]=(float)0.0;
	}
	//Genera números positivos
	for(int i=N_OPERATIONS/2+1; i<=N_OPERATIONS; i++){
	    a[i]=(float)i;
	    b[N_OPERATIONS-i]=(float)i;
	    c[i]=(float)0.0;
	}
	return 0;
}

Resultados del benchmark en la FPU MicroBlaze

Los resultados en bruto de este benchmark ejecutándolos en una tarjeta de desarrollo Basys 3 son los siguientes, donde cada unidad es un tick de reloj, en este caso 10ns.

NMULDIVSQRTPOW
Sin FPU
10182671411117199677285
509027768911812015522457
1001802981374111611979874587
20036039027441132139319565452
FPU Básica
1065292017199677285
5027323980812015522457
100533278051611979874587
200105321545532139319565452
FPU Extendida
105868546181114282
502426367429707928912
10047267199591061650205
2009326142491181063257179

Al promediar y convertir estos datos a FLOPS (Floating point Operations Per Second)

MULDIVSQRTPOW
NONE5527272270609961104
BASIC17845871229637609961104
FULL200699713311411671566583
Resultados benchmark FPU MicroBlazeTM en FLOPS

Los resultados como mejoras porcentuales respecto a las implementaciones software de estas operaciones se muestran en la siguiente imagen, donde además se puede comparar la diferencia entre la FPU básica y extendida.

Comparativa porcentual del benchmark para FPU de MicroBlaze
Comparativa porcentual del benchmark en la FPU de MicroBlaze

Optimizaciones usando la FPU de MicroBlazeTM

No solamente se trata de hardware dedicado cuando se quieren reducir tiempos de ejecución. Un diseño cuidado del código que utiliza dicho hardware es tan importante como el mismo. Se analiza el siguiente ejemplo, el cual es una implementación discretizada del SOGI-PLL del que se habló aquí.

typedef struct Sogi_pll{
	//Parámetros SOGI_PLL
	float k;	    //SOGI k parameter
	float wn;	   //Frequency
	float ts;	   //Sample time
	float b[3];	 //Numerador directa
	float c[3];	 //Numerador cuadratura
	float a[3];	 //Denominador directa y cuadratura
	float kp;	   //Ganancia proporcional PI
	float ki;	   //Ganancia integral PI

	//Señales SOGI_PLL
	float ug[3];       //Señal de entrada
	float yd[3];       //Salida SOGI directa
	float yq[3];       //Salida SOGI cuadratura
	float ud[2];       //Transformada Park (d)-q
	float pi[2];       //Salida PI
	float theta[2];    //Fase salida PLL
	float omega[2];    //Frecuencia salida PLL (rad/s)
	float fg;          //Frecuencia señal
}SOGI_PLL;

void update_sogi_pll(SOGI_PLL *pll, float u_in){
	pll->ug[0] = u_in;
	//Calcula salida del SOGI
	pll->yd[0] = pll->a[1]*pll->yd[1] + pll->a[2]*pll->yd[2] + pll->b[0]*(pll->ug[0] - pll->ug[2]);
	pll->yq[0] = pll->a[1]*pll->yq[1] + pll->a[2]*pll->yq[2] + pll->k*pll->c[0]*(pll->ug[0] + 2*pll->ug[1] + pll->ug[2]);

	//Transformada park
	pll->ud[0] = pll->yd[0]*cosf(pll->theta[0]) + pll->yq[0]*sinf(pll->theta[0]);

	//PI
	pll->pi[0] = pll->pi[1] + pll->kp_pll*pll->ud[0] + (pll->ki_pll*pll->ts-pll->kp_pll)*pll->ud[1];

	//Frecuencia de red
	pll->omega[0] = pll->wn + pll->pi[0];

	//Fase de red (integrador)
	pll->theta[0] = pll->theta[1] + pll->ts*pll->omega[1];

	if(pll->theta[0] > (M_TWOPI)){
		pll->theta[0] -= M_TWOPI;
	}

	pll->fg = pll->omega[0]/M_TWOPI;

	pll->omega[1] = pll->omega[0];  	//Actualiza omega(n-1)
	pll->theta[1] = pll->theta[0];  	//Actualiza theta(n-1)
	pll->pi[1] = pll->pi[0];  			//Actualiza pi_out(n-1)
	pll->ud[1] = pll->ud[0];  			//Actualiza ud(n-1)
	pll->yd[2] = pll->yd[1];  			//Actualiza yd(n-2)
	pll->yd[1] = pll->yd[0];  			//Actualiza yd(n-1)
	pll->yq[2] = pll->yq[1];  			//Actualiza yq(n-2)
	pll->yq[1] = pll->yq[0];  			//Actualiza yq(n-1)
	pll->ug[2] = pll->ug[1];    		 //Actualiza u(n-2)
	pll->ug[1] = pll->ug[0];    		 //Actualiza u(n-1)
}
  • Evitar a toda costa divisiones, aunque pueda parecer una operación sencilla tiene una carga computacional elevada, líneas como la 45 deben sustituirse por
    • pll->fg = pll->omega[0]*(1/M_TWOPI);
  • En la documentación de MicroBlaze se indica que por defecto las constantes con decimales se toman como double, esto es un serio inconveniente no solo por un exceso de precisión en principio no solicitado o innecesario, sino porque la FPU de MicroBlaze solo trabaja con variables en coma flotante de 32bits floats. Se deben castear o específicar todas las constantes que se utilicen, por ejemplo volviendo a la línea 45:
    • pll->fg = pll->omega[0]*(float)(1.0/M_TWOPI);
    • pll->fg = pll->omega[0]*(1.0F/3.1415F);
  • El uso de struct, si en lugar de utilizar esta forma para almacenar y manipular los datos se usan variables «normales», se evita el direccionamiento indirecto, los tiempos de acceso a los mismos se aceleran reduciendo en consecuencia el tiempo de ejecución. Se debe tener en cuenta que esta forma de trabajar puede reducir la legibilidad del código, aumentando el coste de mantenimiento de este.

Resultado de las optimizaciones

El código optimizado aplicando los anteriores puntos quedaría así

//Parámetros SOGI_PLL
float k_sogi;	//SOGI k parameter
float wn;		//Frequency
float ts;		//Sample time
float b_pll[3];	//Numerador directa
float c_pll[3];	//Numerador cuadratura
float a_pll[3];	//Denominator directa y cuadratura
float kp_pll;	//Ganancia proporcional PI
float ki_pll;	//Ganancia integral PI

//Señales SOGI_PLL
float ug[3];	//Señal de entrada
float yd[3];	//Salida SOGI directa
float yq[3];	//Salida SOGI cuadratura
float ud[2];	//Transformada Park (d)-q
float pi[2];	//Salida PI
float theta[2];	//Fase salida PLL
float omega[2]; //Frecuencia salida PLL (rad/s)
float fg;		//Frecuencia señal

float update_sogi_pll(float u_in){
	ug[0] = u_in;
	//Calcula salida del SOGI
	yd[0] = a_pll[1]*yd[1] + a_pll[2]*yd[2] + b_pll[0]*(ug[0] - ug[2]);
	yq[0] = a_pll[1]*yq[1] + a_pll[2]*yq[2] + k_sogi*c_pll[0]*(ug[0] + 2.0F*ug[1] + ug[2]);

	//Transformada park
	ud[0] = yd[0]*cosf(theta[0]) + yq[0]*sinf(theta[0]);

	//PI
	pi[0] = pi[1] + kp_pll*ud[0] + (ki_pll*ts-kp_pll)*ud[1];

	//Frecuencia de red
	omega[0] = wn + pi[0];

	//Fase de red (integrador)
	theta[0] = theta[1] + ts*omega[1];

	if(theta[0] > ((float)M_TWOPI)){
		theta[0] -= (float)M_TWOPI;
	}

	fg = omega[0]*(float)(1.0/M_TWOPI);

	omega[1] = omega[0];  	//Actualiza omega(n-1)
	theta[1] = theta[0];  	//Actualiza theta(n-1)
	pi[1] = pi[0];  		//Actualiza pi_out(n-1)
	ud[1] = ud[0];  		//Actualiza ud(n-1)
	yd[2] = yd[1];  		//Actualiza yd(n-2)
	yd[1] = yd[0];  		//Actualiza yd(n-1)
	yq[2] = yq[1];  		//Actualiza yq(n-2)
	yq[1] = yq[0];  		//Actualiza yq(n-1)
	ug[2] = ug[1];    		//Actualiza u(n-2)
	ug[1] = ug[0];    		//Actualiza u(n-1)

	return theta[0];
}

Al ejecutar este código en bucle, lo cual sería el funcionamiento normal de un PLL, se tendrían las siguientes mejoras en frecuencia de muestreo al ir aplicando optimizaciones

Frecuencias de muestreo de un PLL en MicroBlaze en función de optimizaciones acumuladas
Frecuencias de muestreo de un PLL en MicroBlazeTM en función de optimizaciones acumuladas

En la imagen anterior se han aplicado las optimizaciones de izquierda a derecha, por ejemplo al hacer el casting (float) también se evitó la división. La frecuencia de muestreo Final corresponde a realizar todas las optimizaciones sobre todas las líneas de código, antes de eso solo se habían realizado en una sola línea.

Aunque realizar un benchmark en la FPU de MicroBlazeTM no es una prueba real de funcionamiento, si da pistas sobre los puntos clave donde se debe incidir para realizar optimizaciones. Se debe tener en cuenta que un benchmark solo es útil si se conoce lo que mide, por eso, no son recomendables aquellos sin documentar o con el código fuente oculto, aunque esto último es más típico de benchmark para PC.