Retornando tipos complexos

Já falei por aqui sobre as diversas convenções de chamada para os modos i386 e x86-64 e uma das coisas que disse é que, no caso do modo x86-64, o registrador RAX é sempre usado como valor de retorno. Seja para retornar um simples int, seja para retornar um ponteiro… Mas, o que acontece se quisermos retornar uma estrutura? Note bem, não um ponteiro para uma estrutura, mas toda ela? Vejamos:

struct vertice_s {
  float x, y, z, w; // Coordinate (homogeneous);
  float nx, ny, nz; // Normal vector;
  float r, g, b, a; // Color;
  float u, v;       // Texture coordinate;
};

struct vertice_s assign(void)
{
  struct vertice_s v;

  v.x = v.y = v.z =
  v.nx = v.ny = v.nz =
  v.r = v.g = v.b =
  v.u = v.v = 0.0f;
  v.w = v.a = 1.0f;

  return v;
}

void assign2(struct vertice_s *p)
{
  p->x = p->y = p->z =
  p->nx = p->ny = p->nz =
  p->r = p->g = p->b =
  p->u = p->v = 0.0f;
  p->w = p->a = 1.0f;
}

Aqui, a função assign() apaentemente retorna toda a estrutura local à função. No outro caso, na função assign2 passamos um ponteiro para uma estrutura e lidamos com ele dentro da função. Supreendentemente, ambas as funções fazem quase exatamente a mesma coisa. Isso pode ser observado obtendo a listagem em assembly:

bits 64

section .rodata
float1: dd 1.0

section .text

global assign
global assign2

assign:
  pxor  xmm0, xmm0
  mov   rax, rdi
  movss xmm1, dword [float1]
  movss dword [rdi+12], xmm1
  movss dword [rdi], xmm0
  movss dword [rdi+4], xmm0
  movss dword [rdi+8], xmm0
  movss dword [rdi+16], xmm0
  movss dword [rdi+20], xmm0
  movss dword [rdi+24], xmm0
  movss dword [rdi+28], xmm0
  movss dword [rdi+32], xmm0
  movss dword [rdi+36], xmm0
  movss dword [rdi+40], xmm1
  movss dword [rdi+44], xmm0
  movss dword [rdi+48], xmm0
  ret

assign2:
  pxor  xmm0, xmm0
  movss dword [rdi+48], xmm0
  movss dword [rdi+44], xmm0
  movss dword [rdi+36], xmm0
  movss dword [rdi+32], xmm0
  movss dword [rdi+28], xmm0
  movss dword [rdi+24], xmm0
  movss dword [rdi+20], xmm0
  movss dword [rdi+16], xmm0
  movss dword [rdi+8], xmm0
  movss dword [rdi+4], xmm0
  movss dword [rdi], xmm0
  movss xmm0, dword [float1]
  movss dword [rdi+40], xmm0
  movss dword [rdi+12], xmm0
  ret

A diferença óbvia é que assign retorna o ponteiro para a estrutura apontada por RDI em RAX, como se tivéssemos passado esse ponteiro para a função! A função gasta o mesmo tempo que a sua irmã, assign2, uma vez que, devido à reordenação automática nos processadores mais modernos (Ivy Bridge em diante, pelo menos), mov rax,rdi provavelmente não gastará ciclo algum.

Repare que o compilador é esperto o suficiente para perceber que se quisermos retornar uma estrutura por valor, ela terá que ser assinalada para algum objeto e esse objeto tem um endereço na memória. Se esse objeto estiver alocado na própria pilha (for uma variável local na função chamadora), RDI apontará para a pilha, caso contrário, para uma região no segmento de dados. Eis um exemplo:

extern struct vertice_s assign(void);

// Não nos interessa a imlpementação dessa função agora!
extern void showvertice(struct vertice_s *);

void f(void)
{
  struct vertice_s v;

  v = assign();
  showvertice(&v);
}

O que criará uma listagem mais ou menos assim:

f:
  sub  rsp,136   ; Aloca espaço para a estrutura.
  mov  rdi,rsp
  call assign
  mov  rdi,rsp
  call showvertice
  add  rsp,136   ; Dealoca o espaço.
  ret

Você obterá uma listagem um pouco maior que essa, dependendo das opções de compilação que usar, mas, em essência, é isso o que o compilador faz.

No modo i386 a coisa não é muito diferente. Lembre-se que no modo x86-64 o primeiro argumento da função é passado no registrador RDI. No caso do modo i386, este argumento é passado na pilha (na convenção cdecl), daí o endereço da estrutura estará apontada por RSP+4.

Em resumo: Não tenha medo de retornar estruturas e uniões por valor, é a mesma coisa que passar o ponteiro da estrutura no primeiro argumento. Mas, a coisa é bem diferente de passar argumentos por valor ou referência. Ao fazer:

void f(struct vertice_s v)
{
  struct vertice t;

  t = v;
  ...
}

Todo o conteúdo de v terá que ser copiado para dentro de t (embora o compilador possa decidir não fazê-lo, dependendo do resto da função!). Mas uma coisa é certa! Todo o conteúdo da estrutura terá que ser copiado para a pilha antes da chamada!

Então, tenha medo de passar argumentos por valor e prefira passá-los por referência (ponteiro)… mas, saiba que esse risco você não corre ao retornar por valor…

Anúncios