Existem desvantagens em passar estruturas por valor em C, em vez de passar um ponteiro?

157

Existem desvantagens em passar estruturas por valor em C, em vez de passar um ponteiro?

Se a estrutura é grande, obviamente existe o aspecto de desempenho de copiar muitos dados, mas para uma estrutura menor, deve basicamente ser o mesmo que passar vários valores para uma função.

Talvez seja ainda mais interessante quando usado como valores de retorno. C possui apenas valores de retorno únicos de funções, mas muitas vezes você precisa de vários. Portanto, uma solução simples é colocá-los em uma estrutura e retornar isso.

Existem razões a favor ou contra isso?

Como pode não ser óbvio para todos do que estou falando aqui, darei um exemplo simples.

Se estiver programando em C, mais cedo ou mais tarde você começará a escrever funções parecidas com esta:

void examine_data(const char *ptr, size_t len)
{
    ...
}

char *p = ...;
size_t l = ...;
examine_data(p, l);

Isto não é um problema. O único problema é que você precisa concordar com seu colega de trabalho na ordem em que os parâmetros devem ser, para que você use a mesma convenção em todas as funções.

Mas o que acontece quando você deseja retornar o mesmo tipo de informação? Você normalmente recebe algo parecido com isto:

char *get_data(size_t *len);
{
    ...
    *len = ...datalen...;
    return ...data...;
}
size_t len;
char *p = get_data(&len);

Isso funciona bem, mas é muito mais problemático. Um valor de retorno é um valor de retorno, exceto que nesta implementação não é. Não há como dizer, acima, que a função get_data não tem permissão para ver o que len aponta. E não há nada que faça o compilador verificar se um valor é realmente retornado por esse ponteiro. Então, no próximo mês, quando alguém modificar o código sem entendê-lo corretamente (porque ele não leu a documentação?), Ele será quebrado sem que ninguém perceba, ou começa a falhar aleatoriamente.

Então, a solução que proponho é a estrutura simples

struct blob { char *ptr; size_t len; }

Os exemplos podem ser reescritos assim:

void examine_data(const struct blob data)
{
    ... use data.tr and data.len ...
}

struct blob = { .ptr = ..., .len = ... };
examine_data(blob);

struct blob get_data(void);
{
    ...
    return (struct blob){ .ptr = ...data..., .len = ...len... };
}
struct blob data = get_data();

Por alguma razão, acho que a maioria das pessoas instintivamente faria o examine_data levar um ponteiro para um blob de estrutura, mas não vejo o porquê. Ainda recebe um ponteiro e um número inteiro, é muito mais claro que eles andam juntos. E, no caso get_data, é impossível errar da maneira que descrevi antes, pois não há valor de entrada para o comprimento e deve haver um comprimento retornado.

dkagedal
fonte
Para o que vale, void examine data(const struct blob)está incorreto.
22711 Chris Lutz
Obrigado, alterou para incluir um nome de variável.
precisa saber é o seguinte
1
"Não há como dizer, acima, que a função get_data não tem permissão para analisar o que len aponta. E não há nada que faça o compilador verificar se um valor é realmente retornado por esse ponteiro." - isso não faz sentido para mim (talvez porque seu exemplo seja um código inválido devido às duas últimas linhas que aparecem fora de uma função); por favor você pode elaborar?
Adam Spires
2
As duas linhas abaixo da função estão lá para ilustrar como a função é chamada. A assinatura da função não dá nenhuma dica do fato de que a implementação deve gravar apenas no ponteiro. E o compilador não tem como saber que deve verificar se um valor está gravado no ponteiro, portanto o mecanismo de valor de retorno pode ser descrito apenas na documentação.
dkagedal
1
A principal razão pela qual as pessoas não fazem isso com mais frequência em C é histórica. Antes do C89, você não podia passar ou retornar estruturas por valor; portanto, todas as interfaces do sistema anteriores ao C89 e logicamente deveriam fazê-lo (como gettimeofday) usam ponteiros, e as pessoas tomam isso como exemplo.
Zwol

Respostas:

202

Para estruturas pequenas (por exemplo, point, rect), a passagem por valor é perfeitamente aceitável. Mas, além da velocidade, há outra razão pela qual você deve ter cuidado ao passar / retornar grandes estruturas por valor: espaço na pilha.

Muita programação C é para sistemas embarcados, nos quais a memória é premium e os tamanhos de pilha podem ser medidos em KB ou mesmo em bytes ... Se você estiver passando ou retornando estruturas por valor, cópias dessas estruturas serão colocadas em a pilha, potencialmente causando a situação que este site recebeu o nome de ...

Se eu vir um aplicativo que parece ter uso excessivo de pilha, as estruturas passadas por valor são uma das coisas que procuro primeiro.

Roddy
fonte
2
"Se você estiver passando ou retornando estruturas por valor, cópias dessas estruturas serão colocadas na pilha", eu chamaria de braindead qualquer cadeia de ferramentas que faça isso. Sim, é triste que muitos o façam, mas não é nada que o padrão C exija. Um compilador são otimiza tudo isso.
Restabeleça Monica
1
@KubaOber É por isso que isso não é feito com frequência: stackoverflow.com/questions/552134/…
Roddy
1
Existe uma linha definitiva que separa uma estrutura pequena de uma estrutura grande?
Josie Thompson
63

Um motivo para não fazer isso que não foi mencionado é que isso pode causar um problema em que a compatibilidade binária é importante.

Dependendo do compilador usado, as estruturas podem ser passadas através da pilha ou registradores, dependendo das opções / implementação do compilador

Veja: http://gcc.gnu.org/onlinedocs/gcc/Code-Gen-Options.html

-fpcc-struct-return

-freg-struct-return

Se dois compiladores discordarem, as coisas podem explodir. Desnecessário dizer que os principais motivos para não fazer isso são ilustrados: consumo de pilha e desempenho.

tonylo
fonte
4
Esse era o tipo de resposta que eu estava procurando.
Dkagedal 03/10/08
2
É verdade, mas essas opções não estão relacionadas à passagem por valor. eles se relacionam com estruturas retornadas, o que é completamente diferente. Devolver as coisas por referência é geralmente uma maneira infalível de se atirar nos dois pés. int &bar() { int f; int &j(f); return j;};
Roddy
19

Para realmente responder a essa pergunta, é preciso cavar fundo na área de montagem:

(O exemplo a seguir usa o gcc no x86_64. Qualquer pessoa pode adicionar outras arquiteturas como MSVC, ARM, etc.)

Vamos ter nosso programa de exemplo:

// foo.c

typedef struct
{
    double x, y;
} point;

void give_two_doubles(double * x, double * y)
{
    *x = 1.0;
    *y = 2.0;
}

point give_point()
{
    point a = {1.0, 2.0};
    return a;
}

int main()
{
    return 0;
}

Compile-o com otimizações completas

gcc -Wall -O3 foo.c -o foo

Veja a montagem:

objdump -d foo | vim -

Isto é o que obtemos:

0000000000400480 <give_two_doubles>:
    400480: 48 ba 00 00 00 00 00    mov    $0x3ff0000000000000,%rdx
    400487: 00 f0 3f 
    40048a: 48 b8 00 00 00 00 00    mov    $0x4000000000000000,%rax
    400491: 00 00 40 
    400494: 48 89 17                mov    %rdx,(%rdi)
    400497: 48 89 06                mov    %rax,(%rsi)
    40049a: c3                      retq   
    40049b: 0f 1f 44 00 00          nopl   0x0(%rax,%rax,1)

00000000004004a0 <give_point>:
    4004a0: 66 0f 28 05 28 01 00    movapd 0x128(%rip),%xmm0
    4004a7: 00 
    4004a8: 66 0f 29 44 24 e8       movapd %xmm0,-0x18(%rsp)
    4004ae: f2 0f 10 05 12 01 00    movsd  0x112(%rip),%xmm0
    4004b5: 00 
    4004b6: f2 0f 10 4c 24 f0       movsd  -0x10(%rsp),%xmm1
    4004bc: c3                      retq   
    4004bd: 0f 1f 00                nopl   (%rax)

Excluindo os noplpads, ele give_two_doubles()possui 27 bytes e give_point()29 bytes. Por outro lado, give_point()produz menos uma instrução do quegive_two_doubles()

O interessante é que percebemos que o compilador foi capaz de otimizar movas variantes movapde mais rápidas do SSE2 movsd. Além disso, give_two_doubles()na verdade , os dados entram e saem da memória, o que torna as coisas lentas.

Aparentemente, muito disso pode não ser aplicável em ambientes incorporados (que é onde o campo de jogo para C é a maior parte do tempo atualmente). Como não sou um assistente de montagem, qualquer comentário será bem-vindo!

kizzx2
fonte
6
Contar o número de instruções não é tão interessante, a menos que você possa mostrar uma enorme diferença ou contar aspectos mais interessantes, como o número de saltos difíceis de prever, etc. As propriedades de desempenho reais são muito mais sutis que a contagem de instruções .
dkagedal
6
@dkagedal: Verdadeiro. Em retrospecto, acho que minha própria resposta foi escrita muito mal. Embora eu não tenha me concentrado muito no número de instruções (não sei o que lhe deu essa impressão: P), o ponto real a ser feito é que passar a estrutura por valor é preferível a passar por referência para tipos pequenos. De qualquer forma, a passagem por valor é preferida porque é mais simples (sem malabarismo durante a vida toda, sem necessidade de se preocupar com alguém alterando seus dados ou consto tempo todo) e descobri que não há muita penalidade de desempenho (se não ganho) na cópia por valor , ao contrário do que muitos podem acreditar.
kizzx2
15

A solução simples será retornar um código de erro como valor de retorno e tudo o mais como parâmetro na função.
Esse parâmetro pode ser uma estrutura, é claro, mas não vê nenhuma vantagem específica em passar isso por valor, basta enviar um ponteiro.
Passar estrutura por valor é perigoso, você precisa ter muito cuidado com o que está passando, lembre-se de que não há construtor de cópias em C, se um dos parâmetros da estrutura for um ponteiro, o valor do ponteiro será copiado, pode ser muito confuso e difícil de manter.

Só para completar a resposta (crédito total para Roddy ), o uso da pilha é outro motivo para não passar a estrutura por valor, acredite em mim, a depuração do estouro da pilha é PITA real.

Replay para comentar:

Passagem de struct por ponteiro, o que significa que alguma entidade possui uma propriedade sobre esse objeto e tem um conhecimento completo do que e quando deve ser liberado. Passar struct por valor cria uma referência oculta aos dados internos de struct (ponteiros para outras estruturas etc.). Isso é difícil de manter (possível, mas por quê?).

Ilya
fonte
6
Mas passar um ponteiro não é mais "perigoso" só porque você o coloca em uma estrutura, então eu não o compro.
dkagedal 02/10/08
Ótimo ponto de copiar uma estrutura que contém um ponteiro. Este ponto pode não ser muito óbvio. Para aqueles que não sabem a que ele está se referindo, faça uma pesquisa em cópia profunda versus cópia superficial.
Zooropa
1
Uma das convenções da função C é que os parâmetros de saída sejam listados primeiro antes dos parâmetros de entrada, por exemplo, int func (char * out, char * in);
Zooropa
Você quer dizer como, por exemplo, getaddrinfo () coloca o parâmetro de saída por último? :-) Existem milhares de convenções e você pode escolher o que quiser.
dkagedal
10

Uma coisa que as pessoas aqui se esqueceram de mencionar até agora (ou eu a ignorei) é que as estruturas geralmente têm um preenchimento!

struct {
  short a;
  char b;
  short c;
  char d;
}

Cada caractere é de 1 byte, cada curto é de 2 bytes. Qual é o tamanho da estrutura? Não, não são 6 bytes. Pelo menos não nos sistemas mais usados. Na maioria dos sistemas, será 8. O problema é que o alinhamento não é constante, é dependente do sistema, portanto a mesma estrutura terá alinhamento diferente e tamanhos diferentes em sistemas diferentes.

Não apenas esse preenchimento consome mais sua pilha, mas também aumenta a incerteza de não ser capaz de prever o preenchimento antecipadamente, a menos que você saiba como o sistema funciona e analise cada estrutura que você tem no seu aplicativo e calcule o tamanho por isso. Passar um ponteiro requer uma quantidade previsível de espaço - não há incerteza. O tamanho de um ponteiro é conhecido pelo sistema, é sempre igual, independentemente da aparência da estrutura e do tamanho do ponteiro sempre são escolhidos de maneira que estejam alinhados e não precisem de preenchimento.

Mecki
fonte
2
Sim, mas o preenchimento existe sem depender de passar a estrutura por valor ou por referência.
Ilya
2
@dkagedal: Qual parte de "tamanhos diferentes em sistemas diferentes" você não entendeu? Só porque é assim no seu sistema, você assume que deve ser o mesmo para qualquer outro - é exatamente por isso que você não deve passar por valor. Amostra alterada para que também falhe no seu sistema.
Mecki
2
Acho que os comentários de Mecki sobre o preenchimento de estrutura são relevantes especialmente para sistemas embarcados onde o tamanho da pilha pode ser um problema.
Zooropa
1
Eu acho que o outro lado do argumento é que, se sua estrutura é uma estrutura simples (contendo alguns tipos primitivos), a passagem por valor permitirá que o compilador faça malabarismos usando registradores - enquanto que se você usar ponteiros, as coisas acabarão a memória, que é mais lenta. Isso fica em um nível bastante baixo e depende muito da arquitetura de destino, se algum desses petiscos for importante.
Kizzx2
1
A menos que sua estrutura seja pequena ou sua CPU tenha muitos registros (e as CPUs Intel não), os dados acabam na pilha e isso também é memória e é tão rápido / lento quanto qualquer outra memória. Um ponteiro, por outro lado, é sempre pequeno e apenas um ponteiro, e o ponteiro em si geralmente sempre termina em um registro quando usado com mais frequência.
Mecki 29/07/10
9

Eu acho que sua pergunta resumiu as coisas muito bem.

Uma outra vantagem de passar estruturas por valor é que a propriedade da memória é explícita. Não há dúvida sobre se a estrutura é do heap e quem tem a responsabilidade de liberá-lo.

Darron
fonte
9

Eu diria que passar estruturas (não muito grandes) por valor, tanto como parâmetros quanto como valores de retorno, é uma técnica perfeitamente legítima. É preciso cuidar, é claro, de que a estrutura seja do tipo POD ou que a semântica da cópia seja bem especificada.

Atualização: desculpe, eu estava com meu limite de pensamento em C ++. Lembro-me de uma época em que não era legal em C retornar uma estrutura de uma função, mas isso provavelmente mudou desde então. Eu ainda diria que é válido desde que todos os compiladores que você espera usar suportem a prática.

Greg Hewgill
fonte
Observe que minha pergunta era sobre C, não C ++.
dkagedal
É válido para retornar struct de função simplesmente não :) útil
Ilya
1
Eu gosto da sugestão de llya de usar o retorno como um código de erro e parâmetros para retornar dados da função.
Zooropa
8

Aqui está algo que ninguém mencionou:

void examine_data(const char *c, size_t l)
{
    c[0] = 'l'; // compiler error
}

void examine_data(const struct blob blob)
{
    blob.ptr[0] = 'l'; // perfectly legal, quite likely to blow up at runtime
}

Os membros de um const structsão const, mas se esse membro é um ponteiro (como char *), ele se torna char *constmais do const char *que realmente queremos. Obviamente, podemos assumir que a constdocumentação é de intenção e que qualquer pessoa que viole isso está escrevendo código incorreto (o que é), mas isso não é bom o suficiente para alguns (especialmente aqueles que passaram apenas quatro horas rastreando a causa de uma batida).

A alternativa pode ser fazer um struct const_blob { const char *c; size_t l }e usá-lo, mas isso é bastante confuso - ele entra no mesmo problema de esquema de nomes que eu tenho com typedefos ponteiros. Assim, a maioria das pessoas prefere apenas ter dois parâmetros (ou, mais provavelmente neste caso, usar uma biblioteca de strings).

Chris Lutz
fonte
Sim, é perfeitamente legal, e também algo que você deseja fazer às vezes. Mas concordo que é uma limitação da solução struct que você não pode fazer com que os ponteiros apontem para const.
precisa saber é o seguinte
Uma pegadinha desagradável com a struct const_blobsolução é que, mesmo que haja const_blobmembros que diferem blobapenas da "constância indireta", os tipos struct blob*a struct const_blob*serão considerados distintos para fins de uma regra estrita de alias. Consequentemente, se o código converter de a blob*em a const_blob*, qualquer gravação subsequente na estrutura subjacente usando um tipo invalidará silenciosamente qualquer ponteiro existente do outro tipo, de modo que qualquer uso invoque o comportamento indefinido (que geralmente pode ser inofensivo, mas pode ser mortal) .
precisa
5

A página 150 do Tutorial de montagem do PC em http://www.drpaulcarter.com/pcasm/ tem uma explicação clara sobre como C permite que uma função retorne uma estrutura:

C também permite que um tipo de estrutura seja usado como o valor de retorno de uma função. Obviamente, uma estrutura não pode ser retornada no registro EAX. Compiladores diferentes lidam com essa situação de maneira diferente. Uma solução comum usada pelos compiladores é reescrever internamente a função como aquela que usa um ponteiro de estrutura como parâmetro. O ponteiro é usado para colocar o valor de retorno em uma estrutura definida fora da rotina chamada.

Eu uso o seguinte código C para verificar a instrução acima:

struct person {
    int no;
    int age;
};

struct person create() {
    struct person jingguo = { .no = 1, .age = 2};
    return jingguo;
}

int main(int argc, const char *argv[]) {
    struct person result;
    result = create();
    return 0;
}

Use "gcc -S" para gerar o assembly para este trecho de código C:

    .file   "foo.c"
    .text
.globl create
    .type   create, @function
create:
    pushl   %ebp
    movl    %esp, %ebp
    subl    $16, %esp
    movl    8(%ebp), %ecx
    movl    $1, -8(%ebp)
    movl    $2, -4(%ebp)
    movl    -8(%ebp), %eax
    movl    -4(%ebp), %edx
    movl    %eax, (%ecx)
    movl    %edx, 4(%ecx)
    movl    %ecx, %eax
    leave
    ret $4
    .size   create, .-create
.globl main
    .type   main, @function
main:
    pushl   %ebp
    movl    %esp, %ebp
    subl    $20, %esp
    leal    -8(%ebp), %eax
    movl    %eax, (%esp)
    call    create
    subl    $4, %esp
    movl    $0, %eax
    leave
    ret
    .size   main, .-main
    .ident  "GCC: (Ubuntu 4.4.3-4ubuntu5) 4.4.3"
    .section    .note.GNU-stack,"",@progbits

A pilha antes da chamada é criada:

        +---------------------------+
ebp     | saved ebp                 |
        +---------------------------+
ebp-4   | age part of struct person | 
        +---------------------------+
ebp-8   | no part of struct person  |
        +---------------------------+        
ebp-12  |                           |
        +---------------------------+
ebp-16  |                           |
        +---------------------------+
ebp-20  | ebp-8 (address)           |
        +---------------------------+

A pilha logo após a chamada create:

        +---------------------------+
        | ebp-8 (address)           |
        +---------------------------+
        | return address            |
        +---------------------------+
ebp,esp | saved ebp                 |
        +---------------------------+
Jingguo Yao
fonte
2
Existem dois problemas aqui. O mais óbvio é que isso não descreve "como C permite que uma função retorne uma estrutura". Isso descreve apenas como isso pode ser feito no hardware x86 de 32 bits, que por acaso é uma das arquiteturas mais limitadas quando você analisa o número de registros etc. O segundo problema é que a maneira como os compiladores C geram código para retornar valores é ditado pela ABI (exceto para funções não exportadas ou embutidas). A propósito, as funções embutidas são provavelmente um dos locais onde as estruturas retornadas são mais úteis.
Dkagedal
Obrigado pelas correções. Para obter uma descrição detalhada completa da convenção de chamada, en.wikipedia.org/wiki/Calling_convention é uma boa referência.
Jingguo Yao
@dkagedal: O que é significativo não é apenas que o x86 faz as coisas dessa maneira, mas sim que existe uma abordagem "universal" (ou seja, essa) que permitiria aos compiladores de qualquer plataforma suportar retornos de qualquer tipo de estrutura que não seja ' Tão grande a ponto de explodir a pilha. Embora os compiladores de muitas plataformas usem outros meios mais eficientes para lidar com alguns valores de retorno do tipo estrutura, não há necessidade de o idioma limitar os tipos de retorno da estrutura àqueles com os quais a plataforma pode lidar de maneira ideal.
Supercat
0

Eu só quero apontar uma vantagem de passar suas estruturas por valor é que um compilador de otimização pode otimizar melhor seu código.

Vad
fonte