paulo1205
(usa Ubuntu)
Enviado em 11/12/2017 - 11:24h
Estruturas em C foram projetadas para se parecer bastante com tipos nativos, e parte dessa parecência (sim, essa palavra existe!) é justamente poder ser passado para funções e retornado por valor, em lugar de se ter de recorrer a ponteiros.
Isso significa que o programa abaixo é completamente válido.
#include <stdio.h>
struct X {
int x, y;
};
struct X make_x(void){
struct X result;
result.x=10;
result.y=20;
return result;
}
void print_x(struct X x){
printf("x.x=%d; x.y=%d\n", x.x, x.y);
}
int main(void){
struct X x;
x=make_x();
print_x(x);
return 0;
}
Como isso funciona? Depende de cada compilador e cada arquitetura. Para estruturas suficientemente pequenas, que caibam em um ou dois registradores, a estrutura pode ser inteiramente mapeada nesses registradores, tanto na passagem de argumentos quanto na de valores de retorno. Se a estrutura crescer além disso, pode ser que a passagem de valor no código fonte seja mapeada internamente em passagem por referência. Pode-se também dar um jeito de jogar toda o conteúdo da estrutura na pilha. Eventualmente se pode usar uma área auxiliar, ou outra técnica qualquer, e pode haver híbridos, com a passagem de argumentos usando uma técnica, e o valor de retorno, outra.
No caso do programa acima, quando eu compilo no meu PC com Linux em 64 bits, os dois campos da estrutura são valores de 32 bits, de modo que a estrutura inteira cabe num único registrador. Desse modo, o valor de retorno de make_x () é entregue através do registrador rax e depois, já fora da função, copiado para a região de memória da variável x . No caso de print_x (), o valor do argumento é passado inteiramente dentro do registrador rdi , conforme o uso convencional no UNIX para a arquitetura AMD64.
O mesmo programa, quando compilado em modo de 32 bits da arquitetura i386, que dispõe de menos registradores livres e emprega muito mais a passagem de argumentos através da pilha, a função make_x (), apesar de não ter nenhum parâmetro formal no código fonte, efetivamente recebe o endereço do objeto x (declarado dentro de main ()), e a atribuição de valor de retorno à variável que o recebe é feita dentro do código gerado para make_x (). Parece um tanto anti-intuitivo, mas é o que se pode fazer dadas as limitações. Para print_x (), a estrutura inteira é colocada na pilha antes de se chamar a função.
Se trocarmos os tipos dos campos da estrutura de int para long long (e, correspondentemente, as conversões de printf () de "%d" para "%lld" ), o tamanho total da estrutura passa de 8 bytes (64 bits) para 16 bytes (128 bits). Compilando em modo AMD64, tanto o valor de retorno de make_x () quanto o argumento para print_x () utilizam pares de registradores (rax +rdx e rdi +rsi , respectivamente) para acomodar toda a estrutura. Em modo i386, o comportamento é o mesmo descrito anteriormente (valor de retorno via atribuição dentro da função, com destino passado por referência), e uma quantidade maior de bytes é colocada na pilha na passagem de parâmetro por valor.
Se aumentarmos bastante o tamanho da estrutura, acrescentando-lhe um campo z que seja um array de 1000 elementos do mesmo tipo que os campos x e y , a versão i386 continua passando o tratamento do valor de retorno para dentro do código de make_x (), passando-lhe o destino por referência, e todos aqueles 1002 valores (um para x.x , um para x.y e 1000 para x.z ) são colocados na pilha para ao chamar print_x (). Com uma estrutura agora assim, tão grande, a arquitetura AMD64 tem de adotar abordagem semelhante: make_x () passa a receber o destino da atribuição por referência, por meio do registrador rdi , e print_x () passa a ter os 1002 valores que compõem a estrutura dispostos na pilha.
De todo modo, passar ou retornar estruturas por valor só vale realmente a pena quando ela é suficientemente pequena (e o experimento acima demonstra isso). Tem-se um problema de Engenharia: o acesso a dados indiretamente, através de ponteiros, é mais custoso do que o acesso direto; por outro lado, copiar uma grande quantidade de dados (e passagem por valor implica realizar cópias) pode ser ainda mais custoso, especialmente se a quantidade de acessos aos dados copiados não for muito grande. Para complicar, existem outros fatores que influenciam os resultados: por exemplo, um compilador por ter mecanismos de otimização que escondam ou transformem PPV em PPR ou vice-versa, e um código que se beneficie dessas eventuais otimizações pode esconder uma ineficiência, até o dia em que aquele código, que “sempre funcionou bem”, for compilado com outro compilador ou outros parâmetros e condições de contorno.
Existem casos em que a semântica não deixa duvidas. Às vezes você quer
realmente uma cópia, e deixar isso explícito no código é geralmente melhor do que criar uma função que receba origem e destino por referência e lide com a cópia
de facto apenas internamente. Quase sempre, quanto mais trabalho você puder deixar por conta do compilador, melhor para a sua produtividade.