¡Hola de nuevo!
¿Qué tal lo lleváis? Vamos a seguir con el análisis de puntos críticos de la arquitectura BigInteger.
En esta ocasión, vamos a analizar los puntos críticos de la suma.
¡Vamos allá!
Como ya se explicó entradas atrás, la suma de BigInteger está dividida en varias funciones.
Todo el código involucrado en la suma es el siguiente:
[code lan=gral]
/*
* Función pAdd. Usar para sumar dos números.
*
* Realiza la operación de suma, teniendo en cuenta los signos de los números.
*
* Si los signos son iguales, hace una suma, sino, una resta.
*/
static void pAdd(void *va, void *vb){
struct BigInteger* a =
(struct BigInteger*)malloc(sizeof(struct BigInteger));
struct BigInteger* b =
(struct BigInteger*)malloc(sizeof(struct BigInteger));
if (a == NULL || b == NULL)
showError(4);
memcpy(a, va, sizeof(struct BigInteger));
memcpy(b, vb, sizeof(struct BigInteger));
int sig = signum(a->n[a->count], b->n[b->count]);
//normalizamos los operandos
if(sig == 10)
//a negativo, b positivo. Cambiamos el signo de "a" y hacemos suma
a->n[a->count] *= -1;
else if (sig == 1)
//b negativo, a positivo. Cambiamos el signo de "b" y hacemos suma
b->n[b->count] *= -1;
else if (sig == 11){
//a negativo, b negativo. Cambiamos signos y hacemos suma
a->n[a->count] *= -1;
b->n[b->count] *= -1;
}
//si ambos signos son iguales, se suma, sino, se resta
if (sig == 0 || sig == 11)
addition(a, b);
else
pSub(a, b);
if(sig == 10 || sig == 11)
//en estos casos, siempre se le va la vuelta al signo
a->n[a->count] *= -1;
//ajustamos el resultado
memcpy(va, a, sizeof(struct BigInteger));
//liberamos memoria
free(a);
free(b);
}
/*
* Función add. Usar para sumar dos números.
*
* Realiza la operación de suma, teniendo en cuenta los signos de los números.
*
* Si los signos son iguales, hace una suma, sino, una resta.
*/
void add(void *va, void *vb){
//validamos los datos antes de tratarlos
validateBI(va);
validateBI(vb);
//delegamos en la función estática
pAdd(va, vb);
}
/*
* Función addition.
*
* Simula la operación a = a + b
*/
static void addition(void* va, void* vb) {
struct BigInteger* a =
(struct BigInteger*)malloc(sizeof(struct BigInteger));
struct BigInteger* b =
(struct BigInteger*)malloc(sizeof(struct BigInteger));
int limit;
int min;
int move;
int i;
if (a == NULL || b == NULL)
showError(5);
memcpy(a, va, sizeof(struct BigInteger));
memcpy(b, vb, sizeof(struct BigInteger));
//asumimos que a tiene la mayor longitud
limit = a->count;
//asumimos que b tiene la menor longitud
min = b->count;
//indicador de necesidad de arrastre
move = 0;
i = 0;
//si no es así, rectificamos
if(b->count > limit){
limit = b->count;
min = a->count;
move = 1;
}
//sumamos los dígitos que coinciden
for(;i <= min;i++)
a->n[i] += b->n[i];
//los dígitos que no coinciden los traspasamos
if(move == 1){
for(;i >= limit;i++)
a->n[i] = b->n[i];
a->count = limit;
}
//gestionamos el acarreo
carryAdd(a);
//ajustamos resultado
memcpy(va, a, sizeof(struct BigInteger));
//liberamos memoria
free(a);
free(b);
}
/*
* Función carryAdd.
*
* Gestiona el acarreo de la suma. Si hay movimiento de datos, se mueve el
* valor 1 a ret.
*
* De esta manera, podemos llamar hasta que no haya cambios en el acarreo.
*/
static void carryAdd(void* va) {
int i = 0;
int acc;
struct BigInteger* a =
(struct BigInteger*)malloc(sizeof(struct BigInteger));
if (a == NULL)
showError(6);
memcpy(a, va, sizeof(struct BigInteger));
acc = 0;
//recorremos a y vamos sumando el acarreo
for(;i <= a->count; i++){
//sumamos acarreo
a->n[i] += acc;
//como acc es int, podemos dividir entre 10 y sacar el acarreo
acc = a->n[i] / 10;
if (acc > 0)
//normalizamos el número
a->n[i] = a->n[i] % 10;
}
//si ha quedado acarreo, lo guardamos al final;
if (acc > 0) {
if (a->count == MAX_LENGTH)
showError(1);
else
a->n[++a->count] = acc;
}
//ajustamos resultado
memcpy(va, a, sizeof(struct BigInteger));
//liberamos memoria
free(a);
}
[/code]
A grandes rasgos, la función que a la que se llamaría sería "add". Como ya explicamos, se hará la validación, se obtendrá el signum (ya en pAdd) y se normalizarán los datos, y se pasará a hacer la suma dígito a dígito (en addition).
Finalmente se gestiona el acarreo y se libera memoria.
Bien, pues un análisis de puntos críticos mostraría el siguiente esquema:
La validación de "a" y la validación de "b" se pueden hacer en paralelo (hemos marcado con línea continua, porque la llamada a "validateBI" sería única, y ya veremos cómo se gestiona esta función), ya que en caso de haber un dato erróneo, la ejecución se interrumpe.
Como siempre que hay trabajo paralelo, ponemos un semáforo, y empezamos a trabajar en la normalización.
Una vez llegado el momento de sumar dígito a dígito (addition), podemos abrir dos hilos de clústeres, que sumen la parte común y muevan la parte no común. Ya que la suma se hace posición a posición, y de momento no hay acarreo, se puede hacer totalmente en paralelo, y por tanto, este es un buen punto de optimización.
De nuevo, esperamos a tener la suma completa antes de pasar al acarreo.
Lamentablemente, el acarreo no puede paralelizarse, ya que cada dígito puede ser modificado por el anterior, y por tanto, no se puede paralelizar... Supongamos que en la posición "i" tenemos un "1", y en "i + 1" tenemos un "9": no podemos decir que un "i" normalizado valdrá "1" porque no hay acarreo, ya que si "i + 1" consumiera un acarreo, generaría un nuevo acarreo.
Así, como podéis ver, en la función de la suma estaríamos convirtiendo "n" sumas en una sola, a parte de la gestión del acarreo.
Podemos ver cómo la suma de dígitos es identificada por Visual Studio como un punto de consumo de CPU (en este caso, tras hacer 10.000 sumas de BigInteger). En este caso, supondría bajar un 0,25% el consumo de la suma, lo cual a priori no parece demasiado, pero tengamos en cuenta que esta función se llama desde varios sitios (multiplicación, potencias, raíces, ...)
Bueno, amigos, de momento esto es todo para esta entrada.
¡Nos vemos!
No hay comentarios:
Publicar un comentario