ELO311
Estructuras de Computadores Digitales
Estructuras de Datos
Tomás Arredondo Vidal
Este material está basado en:
material de apoyo del texto de David Patterson, John Hennessy, "Computer Organization & Design", (segunda y tercera edición), Morgan Kaufmann, CA. 2005
material del curso anterior ELO311 del Prof. Leopoldo Silva
Arreglos
Arreglos son una estructura de datos consecutivos en memoria usados para almacenar elementos.
Se requiere una variable entera sin signo, denominada el índice del arreglo, generalmente se emplea un
registro para este índice. (e.g. en C es A[i])
Todas los elementos tienen igual tamaño, típicamente se asume que están almacenadas en forma contigua
En assembler si se conoce la dirección del primero entonces la dirección del elemento i, queda dada por:
Dirección del primero + i * (tamaño del elemento en bytes)
En assembler típicamente el tamaño del elemento es una constante (múltiplo de dos) y la multiplicación
Arreglos (cont)
En C, si se usan índices se inicia con el elemento 0 (e.g. A[0] es el primer elemento del arreglo).
Este modelo de representación de los arreglos en
assembler, es el que usa el lenguaje C, que emplea el nombre del arreglo como un puntero al primer
elemento (e.g. A es la misma dirección que &A[0] para el arreglo A).
C también permite acceder a un elemento vía
indirección, con la expresión A+i, que es la dirección del elemento i del arreglo A.
En C la aritmética de punteros (e.g. A+i) calcula la
posición de memoria del elemento i acuerdo al tamaño de los elementos i (e.g. primer elemento es A + 0,
Arreglos (cont)
Con este modelo es posible ubicar un elemento cualquiera del arreglo sin tener que acceder a los elementos secuencialmente.
Ubicar cualquier elemento del arreglo (e.g. el primero, A[0] o el ultimo: A[99]) toma el mismo tiempo.
Porque tiene la capacidad de acceder a cualquier elemento de memoria de acuerdo a la necesidad del usuario sin tener que hacerlo secuencialmente es que esta estructura se la denomina RAM (Random Access Memory) o Memoria de Acceso Aleatorio.
Ejemplo: Arreglos con Índices
El siguiente segmento, en C, describe la manipulación mediante acceso vía un índice:
int a[ ] = {0,1,2,3,4,5,6}; int i = 0; int k = 0; void main(void) { i = 5; ...
k = a[i]; /* lectura de elemento de arreglo */
...
a[i] = k; /* escritura en elemento de arreglo */
Ejemplo: Arreglos con Índices
En assembler:
.data 0x10010000 # comienzo segmento datos a: .word 0,1,2,3,4,5,6 i: .word 0 k: .word 0 .text .globl main main:
# inicia variable i, en zona estática, con constante 5.
li $t3, 5 # t3=5 Macro que puede escribirse: ori $t3,$zero,5 la $t0, i # t0=&i t0 es un puntero. Apunta a variable i.
# este macro se puede remplazar por un lui y un ori sw $t3, 0($t0) # *t0=t3 Escribe en lo que apunta t0: i=5
Ejemplo: Arreglos con Índices (cont)
#Primero se obtiene en t2, el offset en bytes respecto al inicio. la $t0, i # t0 = &i
lw $t0, 0($t0) # t0 = *t0 o también t0 = i
sll $t2, $t0, 2 # t2 = 4*i
# Luego se forma, en t2, la dirección de a[i]
la $t1, a # t1 = &a[0] = &a, sin usar macro la: lui $t1, 0x1001
# ori $t1, $t1, 0
addu $t2, $t2, $t1 # t2 = &a + 4*i = &( a[i] )
# Finalmente se deposita en t3 el contenido de a[i] lw $t3, 0($t2) # t3 = a[i]
# Los comandos previos pueden escribirse con un macro: lw $t3, a($t2) la $t4, k # t4 = &k
sw $t3, 0($t4) # k = a[i] jr ra
Ejemplo: Arreglos con Punteros
El siguiente segmento, en C, describe la manipulación mediante acceso vía un puntero:
int a[ ] = {0,1,2,3,4,5,6}; int i = 0; int k = 0; void main(void) { i = 5; ...
k = *(a + i); /* lectura de elemento de arreglo */
...
*(a + i) = k; /* escritura en elemento de arreglo */
Ejemplo: Arreglos con Punteros (cont)
En assembler:
.data # por defecto comienza en 0x10010000
a: .word 0,1,2,3,4,5,6 i: .word 0 k: .word 0 .text .globl main main:
# inicia variable i, en zona estática, con constante 5.
li $t3, 5 # t3=5 Macro que puede escribirse: ori $t3,$zero,5
la $t0, i # t0=&i t0 es un puntero. Apunta a variable i.
sw $t3, 0($t0) # *t0=t3 Escribe el contenido de $t3 donde apunta $t0. # Los 2 comandos anteriores pueden escribirse con macro: sw $t3,
Ejemplo: Arreglos con Punteros (cont)
#Primero se obtiene en t2, el offset en bytes respecto al inicio. la $t0, i # t0 = &i
lw $t0, 0($t0) # t0 = *t0 o también t0 = i
sll $t2, $t0, 2 # t2 = 4*i
# Luego se forma, en t2, la dirección de (a+i)
la $t1, a # t1 = &a, sin usar macro la: lui $t1, 0x1001
# ori $t1, $t1, 0
addu $t2, $t2, $t1 # t2 = a+4*i
# Finalmente se deposita en t3 el contenido de *(t2) lw $t3, 0($t2) # t3 = *(t2)
# Los comandos previos pueden escribirse con un macro: lw $t3, a($t2) la $t4, k # t4 = &k
sw $t3, 0($t4) # k = *(a + i); jr ra
Arreglos de Caracteres (Strings)
Arreglos de caracteres (e.g. strings) son arreglos en el cual cada elemento almacenado es un carácter
(típicamente de un byte si es ASCII).
Z-strings (vs L-strings) son los strings usados en C y assembler normalmente, estos strings están
terminados por un carácter NULL ASCII (0x00) como
delimitador.
Al inicializar un string usando “ “ el carácter NULL es insertado automaticamente (e.g. “hola string”) pero de generar strings carácter por carácter hay que insertar el NULL explicitamente.
Ejemplo: Strings
Este ejemplo en C recorta “largo” carácteres de un string, desde la posición “inicial”; y devuelve un puntero al string resultante.
Se le pasa como argumentos la dirección del string y punteros a variables enteras que contienen la posición inicial y el largo.
Entonces, un ejemplo de invocación es: pchar = recortastring(arr, &inicio, &largo);
Recordar que en assembler: los argumentos van en registros $a0, $a1, $a2 y resultado en registro $v0
char *recortastring(register char *s, register int *inicial, register int *largo)
{
s = s + *inicial; *(s + *largo) = '\0'; return s;
Ejemplo: Strings (cont)
# v0 = recortastring($a0, $a1, $a2)
# char *recortastring(register char *s, register int *inicial, register int *largo) recortastring:
# no usa el stack al usar solo registros tN
move $t0, $a0 # t0 = s, macro puede usar: addu $t0, $zero, $a0 lw $t2, 0($a1) # t2 = *inicial
addu $t0, $t0, $t2 # t0 = s + *inicial lw $t1, 0($a2) # t1 = *largo
addu $t1, $t0, $t1 # t1 = s + *largo
sb $zero, 0($t1) # *t1 = '\0' Coloca el carácter nulo de fin de string. move $v0, $t0 # vo = nuevo inicio del string recortado. return(s)
Manipulación de arreglos via índice vs punteros
Existen diferencias en la eficiencia de manipular arreglos usando
índices vs punteros.
void cleararray1(int a[], int celdas) { int i;
for( i = 0; i < celdas ; i++) { a[i] = 0;
} }
Código assembler generado por lcc ($4 = $a0, $5 = $a1) -> .text .ent cleararray1 cleararray1: addu $sp,$sp,-8 sw $30,0($sp)
addu $30, $zero, $zero # i = 0
b L.5 L.2: sll $24,$30,2 # $t8 = 4*i addu $24,$24,$4 # $t8 = a + 4*i sw $0,($24) # Mem($t8) = 0 la $30,1($30) # i = i + 1 L.5: blt $30,$5,L.2 # $ i < a1 -> L.2 lw $30,0($sp) addu $sp,$sp,8 j $31 .end cleararray1
Manipulación de arreglos via índice vs punteros
Versión con punteros 1:
void clearpunt1(int *a , int celdas)
// Versión con punteros
{ int *p;
for( p = a; p < a+celdas ; *p = 0, p++);
}
Código assembler generado por lcc ->
a+celdas es una constante…
.text .ent clearpunt1 clearpunt1: addu $sp,$sp,-8 sw $30,0($sp) addu $30, $zero, $4 # p = a b L.10 L.7: sw $0,($30) # Mem(p) = 0 la $30,4($30) # p = p + 4 L.10: addu $24, $zero, $30 # $t8 = p sll $15,$5,2 # $t7 = 4*celdas addu $15,$15,$4 # $t7 = a + 4*i bltu $24,$15,L.7 # $t8 < t7 -> L.7 lw $30,0($sp) addu $sp,$sp,8 j $31 .end clearpunt1
Manipulación de arreglos via índice vs punteros
Versión con punteros 2:
void clearpunt2(int *a , int celdas)
// Versión con punteros
{ int *p, *q;
for( p = a, q=a+celdas; p < q; p++) *p = 0; }
En general los punteros son mas
eficientes ya que referenciar a+celdas requiere menos instrucciones que a[i] en el lazo interno (inner loop).
.text .ent clearpunt2 clearptr2: addu $sp,$sp,-8 sw $23,0($sp) # guardar $s7 en stack sw $30,4($sp) # guardar $fp en stack addu $30, $zero, $4 # p = a sll $24,$5,2 # $t8 = 4*celdas addu $23,$24,$4 # $s7 = a + 4*i b L.15 L.12: sw $0, ($30) # *p = 0 la $30,4($30) # p = p + 4 L.15: addu $24, $zero, $30 # $t8 = p bltu $24,$23,L.12 # if $t8 < $s7 -> L.12 lw $23,0($sp) lw $30,4($sp) addu $sp,$sp,8 j $31 .end clearpunt2
Estructuras
En C se puede definir estructuras que contienen elementos compuestos de diferentes tipos de datos
(e.g. int, char, float, etc).
Para declarar estas estructuras se usa la palabra
struct
La dirección del primer campo está dada por la dirección de inicio de la estructura.
La dirección del segundo campo está dada por la
dirección de inicio de la estructura + tamaño del primer campo.
La dirección del tercer campo está dada por la dirección de inicio de la estructura + suma de los tamaños del primer y segundo campo.
Puede ser necesario el uso de .space para mantener el alineamiento (ver ejemplo mas adelante)
Ejemplo: Estructuras
En lenguaje C: struct punto { int x; int y; };struct punto a = { 1 , 2}; /* Se inicializan al definir el espacio */
struct punto b = { 3 , 4}; void main(void)
{
a.x=b.x; a.y=b.y; /* Se puede escribir a = b, pero es una */
Ejemplo: Estructuras
En lenguaje C: struct punto { int x; int y; }; struct punto a = { 1 , 2}; struct punto b = { 3 , 4}; void main(void) { a.x=b.x; a.y=b.y; } En assembler: .data structa: .word 1 .word 2 structb: .word 3 .word 4 .text .globl main main:# apuntar a estructuras, registro base con offset
la $t0, structb la $t1, structa lw $t3, 0($t0) # t3=b.x sw $t3, 0($t1) lw $t4, 4($t0) # t4=b.y sw $t4, 4($t1)
Ejemplo: Estructuras con Strings
En lenguaje C: struct pixel{ char color[6]; int lugar[2]; };struct pixel p1 = {"red", {10,20}}; void main(void) { p1.color[0]='g'; p1.color[1]='r'; p1.color[2]='e'; p1.color[3]='e'; p1.color[4]='n'; p1.color[5]='\0'; }
Ejemplo: Estructuras con Strings (cont)
En lenguaje C: struct pixel{ char color[6]; int lugar[2]; };struct pixel p1 = {"red", {10,20}};
void main(void) { p1.color[0]='g'; p1.color[1]='r'; p1.color[2]='e'; p1.color[3]='e'; p1.color[4]='n'; p1.color[5]='\0'; } En assembler: .globl p1 .data .align 2 p1: .byte 114 # 104 = ‘r’ en ASCII
.byte 101 # 101 = ‘e’ en ASCII
.byte 100 # 100 = ‘d’ en ASCII
.byte 0 # 0 = NULL Terminator
.space 2 # 2 bytes de espacio
.space 2 # 2 bytes de espacio
.word 0xa # 10
.word 0x14 # 20
En lenguaje C: struct pixel{
char color[6]; int lugar[2]; };
struct pixel p1 = {"red", {10,20}};
void main(void) { p1.color[0]='g'; p1.color[1]='r'; p1.color[2]='e'; p1.color[3]='e'; p1.color[4]='n'; p1.color[5]='\0'; } En assembler: main: .frame $sp,0,$31 la $24,103 # $t8 = ‘g’ = 103 sb $24,p1 # p1.color[0] = ‘g’ la $24,114 # $t8 = ‘r’ = 114 sb $24,p1+1 # p1.color[1] = ‘r’ la $24,101 # etc... sb $24,p1+2 la $24,101 sb $24,p1+3 la $24,110 sb $24,p1+4 sb $0,p1+5 L.1: j $31 .end main
Manejo dinámico de la memoria
malloc, calloc y free se usan para obtener memoria dinámicamente
Del Linux Programmer's Manual (man malloc):
NAME calloc, malloc, free, realloc - Allocate and free
dynamic memory
SYNOPSIS #include <stdlib.h>
void *calloc(size_t nmemb, size_t size); void *malloc(size_t size);
void free(void *ptr);
DESCRIPTION calloc() allocates memory for an array of nmemb elements of size bytes each and returns a
pointer to the allocated memory. The memory is set to zero. malloc() allocates size bytes and returns a pointer to the allocated memory. The memory is not cleared.
Manejo dinámico de la memoria (cont)
Se puede usar la función estándar sizeof para
determinar el número de bytes de un cierto tipo de datos que se solicitaran al administrador de memoria (parte
del sistema operativo)
sizeof(char *) es igual a 4 (un word) y no refleja el tamaño de un string (usar strlen)
Si es posible asignar espacio retorna un puntero a carácter del primer byte asignado; en caso contrario retorna Null.
El administrador dinámico mantiene datos para asignar y desasignar segmentos de memoria en la zona de
memoria dinámica (denominada heap).
En MIPS por defecto el inicio del segmento datos es 0x10010000 el comienzo del heap es 0x10040000 y el comienzo de datos
Organización de Memoria en MIPS (spim)
Memory 230 words 0000 0000 f f f f f f f c Your Code Reserved OS Static data Mem Map I/O0040 0000 1000 0000 1000 8000 7f f e f f fc Stack Dynamic data $sp $gp PC Kernel Code & Data 8000 0080
Manejo dinámico de la memoria (cont)
El código siguiente ilustra la forma de pedir espacio para un entero y luego libera el espacio asignado.
#include <stdio.h>
#include <stdlib.h> /* Para incluir prototipos de malloc y free */
int main(void) {
int *punt=0; /*Define e inicia puntero*/
punt = (int *) malloc(sizeof(int));/*pide espacio y lo encadena. */
if (punt ==0) return(1); /* si no hay espacio */
*punt=5;
printf("%d\n",*punt);
free(punt); /*Libera espacio*/
return(0); }
Manejo dinámico de la memoria (cont)
Otro ejemplo de asembler MIPS usando servicios: main:
...
li $v0,9 # $v0=9 para pedir memoria en heap (sbrk) li $a0,8 # se pide en bytes (dos palabras)
syscall # heap por defecto en 0x10040000
move $t0,$v0 # retorna en $v0 un puntero al espacio li $t1,0xFFFFFFFF
sw $t1,0($t0) # escribe en las celdas asignadas. sw $t1,4($t0)