Ci sono aspetti negativi nel passare le strutture in base al valore in C, piuttosto che passare un puntatore?


157

Ci sono aspetti negativi nel passare le strutture in base al valore in C, piuttosto che passare un puntatore?

Se la struttura è grande, c'è ovviamente l'aspetto performante della copia di molti dati, ma per una struttura più piccola, dovrebbe sostanzialmente essere lo stesso del passaggio di più valori a una funzione.

È forse ancora più interessante se usato come valori di ritorno. C ha solo valori di ritorno singoli dalle funzioni, ma spesso ne servono diversi. Quindi una soluzione semplice è metterli in una struttura e restituirli.

Ci sono ragioni a favore o contro questo?

Dal momento che potrebbe non essere ovvio per tutti di cosa sto parlando qui, farò un semplice esempio.

Se stai programmando in C, prima o poi inizierai a scrivere funzioni simili a queste:

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

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

Questo non è un problema. L'unico problema è che devi concordare con il tuo collega in quale ordine dovrebbero essere i parametri in modo da utilizzare la stessa convenzione in tutte le funzioni.

Ma cosa succede quando si desidera restituire lo stesso tipo di informazioni? In genere ottieni qualcosa del genere:

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

Funziona bene, ma è molto più problematico. Un valore di ritorno è un valore di ritorno, tranne che in questa implementazione non lo è. Non c'è modo di dire da quanto sopra che la funzione get_data non è autorizzata a guardare a cosa punta len. E non c'è nulla che faccia verificare al compilatore che un valore sia effettivamente restituito tramite quel puntatore. Quindi il mese prossimo, quando qualcun altro modifica il codice senza comprenderlo correttamente (perché non ha letto la documentazione?), Si rompe senza che nessuno se ne accorga o inizia a bloccarsi in modo casuale.

Quindi, la soluzione che propongo è la semplice struttura

struct blob { char *ptr; size_t len; }

Gli esempi possono essere riscritti in questo modo:

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();

Per qualche ragione, penso che la maggior parte delle persone farebbe istintivamente fare esaminare_data come un puntatore a un blob strutt, ma non vedo perché. Ottiene ancora un puntatore e un numero intero, è molto più chiaro che vanno insieme. E nel caso get_data è impossibile sbagliare nel modo che ho descritto prima, poiché non esiste un valore di input per la lunghezza e deve esserci una lunghezza restituita.


Per quello che vale, void examine data(const struct blob)non è corretto.
Chris Lutz,

Grazie, l'ho cambiato per includere un nome variabile.
dkagedal,

1
"Non c'è modo di dire da quanto precede che la funzione get_data non è autorizzata a guardare a cosa punta len. E non c'è nulla che faccia verificare al compilatore che un valore sia effettivamente restituito attraverso quel puntatore." - questo non ha alcun senso per me (forse perché il tuo esempio è un codice non valido a causa delle ultime due righe che appaiono al di fuori di una funzione); per favore puoi elaborare?
Adam Spires,

2
Le due righe sotto la funzione sono lì per illustrare come viene chiamata la funzione. La firma della funzione non fornisce alcun suggerimento sul fatto che l'implementazione dovrebbe scrivere solo sul puntatore. E il compilatore non ha modo di sapere che dovrebbe verificare che un valore sia scritto nel puntatore, quindi il meccanismo del valore restituito può essere descritto solo nella documentazione.
dkagedal,

1
Il motivo principale per cui le persone non lo fanno più spesso in C è storico. Prima di C89, non era possibile passare o restituire le strutture in base al valore, quindi tutte le interfacce di sistema che precedono C89 e logicamente dovrebbero farlo (come gettimeofday) utilizzare invece i puntatori e le persone lo prendono come esempio.
zwol,

Risposte:


202

Per le piccole strutture (ad es. Punto, rettangolo) il passaggio per valore è perfettamente accettabile. Ma, a parte la velocità, c'è un'altra ragione per cui dovresti stare attento a passare / restituire grandi strutture per valore: Stack space.

Molta programmazione in C è per i sistemi embedded, dove la memoria è un premio, e le dimensioni dello stack possono essere misurate in KB o anche Byte ... Se si passano o si restituiscono le strutture in base al valore, le copie di tali strutture verranno posizionate su lo stack, causando potenzialmente la situazione che questo sito prende il nome da ...

Se vedo un'applicazione che sembra avere un uso eccessivo dello stack, le strutture passate per valore sono una delle cose che cerco prima.


2
"Se stai passando o restituendo le strutture in base al valore, le copie di tali strutture verranno messe in pila" chiamerei braindead qualsiasi toolchain che lo fa. Sì, è triste che così tanti lo faranno, ma non è qualcosa che lo standard C richiede. Un compilatore sano ottimizzerà tutto.
Ripristina Monica il

1
@KubaOber Questo è il motivo per cui ciò non viene fatto spesso: stackoverflow.com/questions/552134/…
Roddy

1
Esiste una linea definitiva che separa una piccola struttura da una grande struttura?
Josie Thompson,

63

Un motivo per non farlo, che non è stato menzionato, è che ciò può causare un problema in cui la compatibilità binaria è importante.

A seconda del compilatore utilizzato, le strutture possono essere passate attraverso lo stack o i registri a seconda delle opzioni / implementazione del compilatore

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

-fpcc-struct ritorno

-freg-struct ritorno

Se due compilatori non sono d'accordo, le cose possono esplodere. Inutile dire che i motivi principali per non farlo sono illustrati come consumo di stack e motivi di prestazioni.


4
Questo era il tipo di risposta che stavo cercando.
dkagedal,

2
Vero, ma quelle opzioni non si riferiscono al valore pass-by. si riferiscono a strutture di ritorno che è una cosa completamente diversa. Restituire le cose per riferimento è di solito un modo sicuro di spararti in entrambi i piedi. int &bar() { int f; int &j(f); return j;};
Roddy,

19

Per rispondere davvero a questa domanda, è necessario scavare in profondità nella terra dell'assemblea:

(L'esempio seguente usa gcc su x86_64. Chiunque può aggiungere altre architetture come MSVC, ARM, ecc.)

Facciamo il nostro programma di esempio:

// 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;
}

Compilalo con ottimizzazioni complete

gcc -Wall -O3 foo.c -o foo

Guarda l'assemblea:

objdump -d foo | vim -

Questo è ciò che otteniamo:

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)

Escludendo i noplpad, give_two_doubles()ha 27 byte mentre give_point()ha 29 byte. D'altra parte, give_point()fornisce un'istruzione in meno rispetto agive_two_doubles()

La cosa interessante è che notiamo che il compilatore è stato in grado di ottimizzare movnelle varianti SSE2 più veloci movapde movsd. Inoltre, give_two_doubles()sposta effettivamente i dati dentro e fuori dalla memoria, il che rende le cose lente.

Apparentemente gran parte di questo potrebbe non essere applicabile in ambienti embedded (che è dove il campo di gioco per C è la maggior parte delle volte al giorno d'oggi). Non sono una procedura guidata di assemblaggio, quindi qualsiasi commento sarebbe il benvenuto!


6
Contare il numero di istruzioni non è poi così interessante, a meno che tu non possa mostrare un'enorme differenza o contare aspetti più interessanti come il numero di salti difficili da prevedere, ecc. Le proprietà effettive delle prestazioni sono molto più sottili del conteggio delle istruzioni .
dkagedal,

6
@dkagedal: True. In retrospettiva, penso che la mia risposta sia stata scritta molto male. Anche se non mi sono concentrato molto sul numero di istruzioni (non so cosa ti ha dato quell'impressione: P), il punto reale da sottolineare è che passare la struttura per valore è preferibile passare per riferimento per i piccoli tipi. In ogni caso, è preferibile passare per valore perché è più semplice (nessuna giocoleria a vita, nessuna necessità di preoccuparsi che qualcuno cambi i tuoi dati o consttutto il tempo) e ho scoperto che non c'è molta penalità delle prestazioni (se non guadagno) nella copia pass-by-value , contrariamente a quanto molti potrebbero credere.
kizzx2,

15

La soluzione semplice sarà restituire un codice di errore come valore di ritorno e tutto il resto come parametro nella funzione,
questo parametro può essere una struttura ovviamente ma non si vede alcun vantaggio particolare passando questo per valore, appena inviato un puntatore.
Passare la struttura per valore è pericoloso, devi stare molto attento a ciò che stai passando, ricorda che non esiste un costruttore di copie in C, se uno dei parametri della struttura è un puntatore il valore del puntatore verrà copiato potrebbe essere molto confuso e difficile da mantenere.

Solo per completare la risposta (merito completo a Roddy ) l'utilizzo dello stack è un'altra ragione per non passare la struttura per valore, credetemi, il debug dello overflow dello stack è una vera PITA.

Riproduci per commentare:

Passare la struttura per puntatore significa che alcune entità hanno una proprietà su questo oggetto e hanno una conoscenza completa di cosa e quando dovrebbero essere rilasciati. Il passaggio di struct in base al valore crea riferimenti nascosti ai dati interni di struct (puntatori ad altre strutture ecc.) In questo è difficile da mantenere (possibile ma perché?).


6
Ma passare un puntatore non è più "pericoloso" solo perché lo metti in una struttura, quindi non lo compro.
dkagedal,

Ottimo punto sulla copia di una struttura che contiene un puntatore. Questo punto potrebbe non essere molto ovvio. Per coloro che non sanno a cosa si sta riferendo, fai una ricerca su copia profonda vs copia superficiale.
zooropa,

1
Una delle convenzioni della funzione C prevede che i parametri di output siano elencati prima dei parametri di input, ad esempio int func (char * out, char * in);
zooropa,

Intendi come, ad esempio, come getaddrinfo () metta l'ultimo parametro di output? :-) Ci sono migliaia di convenzioni e puoi scegliere quello che vuoi.
dkagedal,

10

Una cosa che la gente qui ha dimenticato di menzionare finora (o l'ho trascurato) è che le strutture di solito hanno un'imbottitura!

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

Ogni carattere è 1 byte, ogni valore breve è 2 byte. Quanto è grande la struttura? No, non sono 6 byte. Almeno non su altri sistemi più comunemente usati. Sulla maggior parte dei sistemi sarà 8. Il problema è che l'allineamento non è costante, dipende dal sistema, quindi la stessa struttura avrà allineamenti diversi e dimensioni diverse su sistemi diversi.

Non solo il padding consumerà ulteriormente il tuo stack, ma aggiunge anche l'incertezza di non essere in grado di prevederlo in anticipo, a meno che tu non sappia come i pad di sistema e poi guardi ogni singola struttura che hai nella tua app e calcoli le dimensioni per questo. Passare un puntatore richiede una quantità prevedibile di spazio - non c'è incertezza. La dimensione di un puntatore è nota per il sistema, è sempre uguale, indipendentemente dall'aspetto della struttura e le dimensioni del puntatore vengono sempre scelte in modo tale da essere allineate e non necessitano di riempimento.


2
Sì, ma l'imbottitura esiste senza alcuna dipendenza dal passaggio della struttura per valore o per riferimento.
Ilya,

2
@dkagedal: quale parte di "dimensioni diverse su sistemi diversi" non hai capito? Solo perché è così sul tuo sistema, supponi che debba essere lo stesso per tutti gli altri - è esattamente per questo che non dovresti passare per valore. Campione modificato in modo che non riesca anche sul tuo sistema.
Mecki,

2
Penso che i commenti di Mecki sull'imbottitura della struttura siano rilevanti soprattutto per i sistemi embedded in cui le dimensioni dello stack possono essere un problema.
zooropa,

1
Immagino che il rovescio della medaglia dell'argomento sia che se la tua struttura è una semplice struttura (contenente un paio di tipi primitivi), passare per valore consentirà al compilatore di destreggiarsi usando i registri - mentre se usi i puntatori, le cose finiscono in la memoria, che è più lenta. Questo diventa piuttosto basso e dipende praticamente dalla tua architettura di destinazione, se una qualsiasi di queste cose importanti è importante.
kizzx2,

1
A meno che la tua struttura non sia piccola o la tua CPU abbia molti registri (e le CPU Intel no), i dati finiscono nello stack e che è anche memoria e veloce / lento come qualsiasi altra memoria. Un puntatore invece è sempre piccolo e solo un puntatore e il puntatore stesso di solito finisce sempre in un registro quando viene usato più spesso.
Mecki,

9

Penso che la tua domanda abbia riassunto abbastanza bene le cose.

Un altro vantaggio del passaggio di strutture in base al valore è che la proprietà della memoria è esplicita. Non c'è da chiedersi se la struttura proviene dal mucchio e chi ha la responsabilità di liberarla.


9

Direi che passare strutture (non troppo grandi) per valore, sia come parametri sia come valori di ritorno, è una tecnica perfettamente legittima. Bisogna fare attenzione, ovviamente, che la struttura sia di tipo POD o che la semantica della copia sia ben specificata.

Aggiornamento: mi dispiace, avevo il mio cappello pensante in C ++. Ricordo un momento in cui in C non era legale restituire una struttura da una funzione, ma da allora probabilmente è cambiata. Direi comunque che è valido fintanto che tutti i compilatori che prevedi di utilizzare supportano la pratica.


Nota che la mia domanda riguardava C, non C ++.
dkagedal,

È valido per restituire struct dalla funzione semplicemente non utile :)
Ilya,

1
Mi piace il suggerimento di llya di utilizzare il ritorno come codice di errore e parametri per restituire i dati dalla funzione.
zooropa,

8

Ecco qualcosa che nessuno ha menzionato:

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
}

I membri di un const structsono const, ma se quel membro è un puntatore (come char *), diventa char *constpiuttosto che quello const char *che vogliamo davvero. Certo, potremmo presumere che si consttratti della documentazione di intenti e che chiunque violi questo stia scrivendo un codice errato (che sono), ma questo non è abbastanza buono per alcuni (specialmente quelli che hanno appena trascorso quattro ore a rintracciare la causa di un in crash).

L'alternativa potrebbe essere quella di crearne una struct const_blob { const char *c; size_t l }e usarla, ma è piuttosto disordinata: si trova nello stesso problema di schema di denominazione che ho con typedefi puntatori. Pertanto, la maggior parte delle persone si limita ad avere solo due parametri (o, più probabilmente per questo caso, usando una libreria di stringhe).


Sì, è perfettamente legale, e anche qualcosa che vuoi fare a volte. Ma sono d'accordo che è un limite della soluzione struct che non è possibile creare i puntatori che puntano a const.
dkagedal,

Un brutto gotcha con la struct const_blobsoluzione è che anche se const_blobha membri che differiscono blobsolo dalla "costanza indiretta", i tipi struct blob*a a struct const_blob*saranno considerati distinti ai fini di una rigorosa regola di aliasing. Di conseguenza, se il codice lancia da a blob*a a const_blob*, qualsiasi scrittura successiva sulla struttura sottostante usando un tipo invaliderà silenziosamente qualsiasi puntatore esistente dell'altro tipo, in modo tale che qualsiasi uso invocherà un comportamento indefinito (che di solito può essere innocuo, ma potrebbe essere mortale) .
supercat,

5

La pagina 150 di PC Assembly Tutorial su http://www.drpaulcarter.com/pcasm/ contiene una chiara spiegazione di come C consente a una funzione di restituire una struttura:

C consente inoltre di utilizzare un tipo di struttura come valore di ritorno di una funzione. Ovviamente una struttura non può essere restituita nel registro EAX. Diversi compilatori gestiscono questa situazione in modo diverso. Una soluzione comune utilizzata dai compilatori è quella di riscrivere internamente la funzione come una che accetta un puntatore a struttura come parametro. Il puntatore viene utilizzato per inserire il valore restituito in una struttura definita al di fuori della routine chiamata.

Uso il seguente codice C per verificare la dichiarazione precedente:

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;
}

Utilizzare "gcc -S" per generare assembly per questo pezzo di codice 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

Lo stack prima della chiamata crea:

        +---------------------------+
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)           |
        +---------------------------+

Lo stack subito dopo aver chiamato crea:

        +---------------------------+
        | ebp-8 (address)           |
        +---------------------------+
        | return address            |
        +---------------------------+
ebp,esp | saved ebp                 |
        +---------------------------+

2
Ci sono due problemi qui. Il più ovvio è che questo non descrive affatto "come C consente a una funzione di restituire una struttura". Questo descrive solo come può essere fatto su hardware x86 a 32 bit, che risulta essere una delle architetture più limitate quando si guarda il numero di registri ecc. Il secondo problema è che il modo in cui i compilatori C generano codice per restituire valori è dettato dall'ABI (ad eccezione delle funzioni non esportate o incorporate). A proposito, le funzioni incorporate sono probabilmente uno dei luoghi in cui le strutture di ritorno sono più utili.
dkagedal,

Grazie per le correzioni. Per un dettaglio completo della convenzione di chiamata, en.wikipedia.org/wiki/Calling_convention è un buon riferimento.
Jingguo Yao,

@dkagedal: Ciò che è significativo non è solo che x86 capita di fare le cose in questo modo, ma piuttosto che esiste un approccio "universale" (cioè questo) che consentirebbe ai compilatori di qualsiasi piattaforma di supportare ritorni di qualsiasi tipo di struttura che non è " t così grande da far saltare la pila. Mentre i compilatori per molte piattaforme useranno altri mezzi più efficienti per gestire alcuni valori di ritorno di tipo struttura, non è necessario che la lingua limiti i tipi di ritorno di struttura a quelli che la piattaforma può gestire in modo ottimale.
supercat,

0

Voglio solo indicare un vantaggio del passaggio delle tue strutture in base al valore è che un compilatore di ottimizzazione può ottimizzare meglio il tuo codice.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.