Do alto-nível ao
assembly
Compiladores
Viagem
Como são implementadas as estruturas
computacionais em assembly?
Revisão dos conceitos relacionados com a
programação em assembly para o MIPS
R3000 (ver disciplina de Arquitectura de
Computadores)
Do alto-nível ao assembly
Alvo: MIPS R3000
int sum(int A[], int N) {
int i, sum = 0; for(i=0; i<N; i++) { sum = sum + A[i]; }
return sum; }
# $a0 armazena o endereço de A[0] # $a1 armazena o valor de N
Sum: addi $t0, $0, 0 # i = 0 addi $v0, $0, 0 # sum = 0
Loop: beq $t0, $a1, End # if(i == N) goto End; add $t1, $t0, $t0 # 2*i
add $t1, $t1, $t1 # 2*(2*i) = 4*i add $t1, $t1, $a0 # 4*i + base(A) lw $t2, 0($t1) # load A[i]
add $v0, $v0, $t2 # sum = sum + A[i] addi $t0, $t0, 1 # i++
j Loop # goto Loop;
Do alto-nível ao assembly
Variáveis globais:
Armazenadas na memória
Para cada uso de uma variável global
o compilador tem de gerar instruções
load/store
int a, b, c;
... fun(...) {
...
}
Memória
a
b
c
Variáveis Globais
int a, b, c;
void fun() {
c = a + b;
}
.data 0x10000000 a: .space 4 b: .space 4 c: .space 4 la $t1, a lw $t1, 0($t1) la $t2, b lw $t2, 0($t2) add $t3, $t2, $t1 la $t4, c sw $t3, 0($t4) … 0x10000008 0x10000004 0x10000000Alocação
de
memória
Memória
a
b
c
Do alto-nível ao assembly
Conceito de chamada a procedimentos
Cada procedimento tem estados
• Variáveis locais
• Endereço de retorno
Estado é guardado na área de memória designada por
pilha de chamadas (é utilizado um registo para apontar para a posição actual da pilha)
Pilha de chamadas
A pilha de chamadas encontra-se no topo da memória
A pilha cresce para baixo
void fun() {
int a, b, c;
...
c = a + b;
...
}
Memória
c
b
a
Registos
$sp
Variáveis Locais
Exemplo:
void fun() {
int a, b, c;
...
c = a + b;
...
}
fun: addi $sp, $sp, -12 … lw $t1, 0($sp) lw $t2, 4($sp) add $t3, $t2, $t1 sw $t3, 8($sp) … addi $sp, $sp, 12 jr $raMemória
c
b
a
$sp
Reserva espaço na pilha liberta espaço na pilha Load a Load b store c a + bVariáveis Locais
Acesso aos registos internos do processador é muito
mais rápido
Mas os registos internos são em número limitado
E por isso nem todas as variáveis locais podem ser
armazenadas nesses registos
No passado a atribuição de registos internos do
processador a variáveis locais era feita pelo
programador:
A linguagem C tem uma palavra reservada para
orientar o compilador:
register
(e.g., register int c;)
Hoje os compiladores são muito mais eficientes
Variáveis Locais
Utilização de registos internos
void fun() {
int a, b, c;
...
c = a + b;
...
}
fun: … add $t3, $t2, $t1 … jr $raFicheiro de
Registos
$t1
$t2
$t3
a
b
c
Do alto-nível ao assembly
Implementar Registos
Registos contêm
vários campos
Cada estrutura é
armazenada em
posições contíguas de
memória
typedef struct {
int x, y, z;
} foo;
foo *p;
Memória
z
y
x
p
Do alto-nível ao assembly
Exemplo com
estrutura
local:
typedef struct {
int x, y, z;
} foo;
fun() {
foo *p;
p->x = p->y + p->z;
fun: addi $sp, $sp, -16 … lw $t1, 0($sp) addi $t1, $t1, 8 lw $t2, 0($t1) lw $t1, 0($sp) addi $t1, $t1, 12 lw $t3, 0($t1) add $t3, $t2, $t3 lw $t1, 0($sp) addi $t1, $t1, 4 sw $t3, 0($t1) … addi $sp, $sp, 16 Reserva espaço na pilha liberta espaço Endereço de p Load p->y Load p->z p->y + p->z store em p->xMemória
z
y
x
p
address p->y address p->z address p->x Endereço de p Endereço de pDo alto-nível ao assembly
Exemplo com estrutura local
(optimizado):
typedef struct {
int x, y, z;
} foo;
fun() {
foo *p;
p->x = p->y + p->z;
fun: addi $sp, $sp, -16 … lw $t2, 8($sp) lw $t3, 12($sp) add $t3, $t2, $t3 sw $t3, 4($sp) … addi $sp, $sp, 16 … Reserva espaço na pilha liberta espaço na pilha Load p->y Load p->z p->y + p->z store em p->xMemória
z
y
x
p
Do alto-nível ao
assembly
Compiladores
Alinhamento, empacotamento e
enchimento
Requisitos de alinhamento:
Inteiros tipo
int
(4 bytes) a começar em
endereços com os 2 LSBs == “00”
Inteiros tipo
short
(2 bytes) a começar em
endereços com o LSB == ‘0’
Alinhamento requer:
Enchimento (padding) entre campos para
assegurar o alinhamento
Empacotamento de campos para assegurar
a utilização de memória
Alinhamento
typedef struct {
int w;
char x;
int y;
char z;
} foo;
foo *p;
Memória
z
y
x
p
w
Memória
y
x, z
p
w
Organização
“ingénua”
Organização
Empacotada
(poupa 4 bytes)
Arrays
Afectação de posições
de memória para os
elementos do array
Elementos são
armazenados
contiguamente
Memória
a[0]
int a[4];
a[1]
a[2]
a[3]
Memória
a[1]
short a[4];
a[0]
a[3]
a[2]
Arrays
Utilizando
registos do
processador
para
armazenar as
variáveis i e j:
.data
A:
.space 16
.text
Proc:
…
la
$t0, A
addi
$t2, $0, 4
mult
$t1, $t2
mflo
$t2
add
$t3, $t2, $t0
lw
$t4, 0($t3)
int a[4];
proc() {
int i, j;
…
i = a[j];
…
}
Expressões
a = b * c + d – e;
a
em $t4;
b
em $t0;
c
em $t1;
d
em
$t2;
e
em $t3
…
mult
$t0, $t1
mflo
$t4
sub
$t5, $t2, $t3
add
$t4, $t4, $t5
…
…
mult
$t0, $t1
mflo
$t4
add
$t4, $t4, $t2
sub
$t4, $t4, $t3
…
Estruturas condicionais
If(a == 1) b = 2;
a
em $t0;
b
em $t1
If(a == 1) b = 2;
else b = 1;
a
em $t2;
b
em $t1
…
addi
$t2, $0, 1
bne
$t2, $t0, skip_if
addi
$t1, $0, 2
skip_if: …
…
addi
$t2, $0, 1
bne
$t2, $t0, else
addi
$t1, $0, 2
j
skip_if
else: addi
$t1, $0, 1
Estruturas condicionais
Branch-delay
O processador executa
sempre a instrução a
seguir a uma instrução
de salto (quer o salto
seja realizado ou não)
Quando não é possível
deslocar uma instrução
para depois da instrução
de salto, tem de se
introduzir uma instrução
nop
if(a == 1) b = 2;
c = a+1;
a
em $t0;
b
em $t1
…
addi
$t2, $0, 1
bne
$t2, $t0, skip_if
addi
$t3, $t0, 1
addi
$t1, $0, 2
skip_if: …
Ciclos
Transformar o fluxo de controlo
(while, for, do while, etc.) em saltos
int sum(int A[], int N) { int i, sum = 0;
for(i=0; i<N; i++) { sum = sum + A[i]; }
return sum; }
# $a0 armazena o endereço de A[0] # $a1 armazena o valor de N
Sum: addi $t0, $0, 0 # i = 0 addi $v0, $0, 0 # sum = 0
Loop: beq $t0, $a1, End # if(i == N) goto End; add $t1, $t0, $t0 # 2*i
add $t1, $t1, $t1 # 2*(2*i) = 4*i add $t1, $t1, $a0 # 4*i + base(A) lw $t2, 0($t1) # load A[i]
add $v0, $v0, $t2 # sum = sum + A[i] addi $t0, $t0, 1 # i++
Ciclos
Otimizações
Manter i e endereço de a[i] em registos
Determinar endereço de a[0] antes do corpo
do ciclo, e incrementar 4 (no caso de serem
acessos a palavras de 32 bits) no corpo do
ciclo
Caso o ciclo execute pelo menos uma
iteração (N > 0) passar salto do ciclo para o
fim do corpo
Ciclos
Código após as optimizações
int sum(int A[], int N) { int i, sum = 0;
for(i=0; i<N; i++) { sum = sum + A[i]; }
return sum; }
# $a0 armazena o endereço de A[0] # $a1 armazena o valor de N
Sum: Addi $t0, $0, 0 # i = 0 Addi $v0, $0, 0 # sum = 0 Loop: Lw $t2, 0($a0) # load A[i]
Add $v0, $v0, $t2 # sum = sum + A[i] addi $a0, $a0, 4 # add 4 to address Addi $t0, $t0, 1 # i++
bne $t0, $a1, Loop # if(i != N) goto Loop;
Procedimentos
Protocolo entre os procedimentos que
invocam e os procedimentos invocados
Dependente do processador
No MIPS:
•
Procedimento espera argumentos nos registos
$a0-$a3
•
Coloca valores a retornar nos registos $v0-$v1
•
Outras formas de passagem de parâmetros
utilizam a pilha de chamadas (por exemplo,
sempre que o número de argumentos ultrapassa
o número de registos para utilizar como
Sumário
Quais as responsabilidades do compilador?
Esconder
do programador conceitos baixo-nível da
máquina
Produzir
rapidamente
código eficiente
Afetar
variáveis a registos locais ou posições de
memória
Cálculo
de expressões com constantes
Manter
funcionalidade inicial