¿Hay algún inconveniente en pasar estructuras por valor en C, en lugar de pasar un puntero?


157

¿Hay algún inconveniente en pasar estructuras por valor en C, en lugar de pasar un puntero?

Si la estructura es grande, obviamente existe el aspecto de rendimiento de copiar muchos datos, pero para una estructura más pequeña, básicamente debería ser lo mismo que pasar varios valores a una función.

Quizás sea aún más interesante cuando se usa como valores de retorno. C solo tiene valores de retorno únicos de las funciones, pero a menudo necesita varios. Entonces, una solución simple es ponerlos en una estructura y devolver eso.

¿Hay alguna razón a favor o en contra de esto?

Como podría no ser obvio para todos de lo que estoy hablando aquí, daré un ejemplo simple.

Si está programando en C, tarde o temprano comenzará a escribir funciones que se ven así:

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

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

Esto no es un problema El único problema es que tiene que estar de acuerdo con su compañero de trabajo en el orden en que deben estar los parámetros para que use la misma convención en todas las funciones.

Pero, ¿qué sucede cuando quieres devolver el mismo tipo de información? Por lo general, obtienes algo como esto:

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

Esto funciona bien, pero es mucho más problemático. Un valor de retorno es un valor de retorno, excepto que en esta implementación no lo es. No hay forma de saber por lo anterior que la función get_data no puede ver a qué apunta len. Y no hay nada que haga que el compilador compruebe que un valor realmente se devuelve a través de ese puntero. Entonces, el próximo mes, cuando alguien más modifica el código sin entenderlo correctamente (¿porque no leyó la documentación?), Se rompe sin que nadie lo note, o comienza a fallar al azar.

Entonces, la solución que propongo es la estructura simple

struct blob { char *ptr; size_t len; }

Los ejemplos se pueden reescribir así:

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 alguna razón, creo que la mayoría de la gente instintivamente haría que exam_data tomara un puntero a un blob de estructura, pero no veo por qué. Todavía obtiene un puntero y un número entero, es mucho más claro que van juntos. Y en el caso de get_data, es imposible equivocarse de la manera que describí anteriormente, ya que no hay un valor de entrada para la longitud, y debe haber una longitud devuelta.


Por lo que vale, void examine data(const struct blob)es incorrecto.
Chris Lutz

Gracias, lo cambió para incluir un nombre de variable.
dkagedal

1
"No hay forma de deducir de lo anterior que la función get_data no puede ver a qué apunta len. Y no hay nada que haga que el compilador verifique que un valor realmente se devuelve a través de ese puntero". - esto no tiene ningún sentido para mí (tal vez porque su ejemplo es un código no válido debido a que las dos últimas líneas aparecen fuera de una función); por favor puedes explicar?
Adam Spires

2
Las dos líneas debajo de la función están allí para ilustrar cómo se llama la función. La firma de la función no da pistas sobre el hecho de que la implementación solo debería escribir en el puntero. Y el compilador no tiene forma de saber que debe verificar que se escriba un valor en el puntero, por lo que el mecanismo del valor de retorno solo se puede describir en la documentación.
dkagedal

1
La razón principal por la que las personas no hacen esto con más frecuencia en C es histórica. Antes de C89, no podía pasar o devolver estructuras por valor, por lo que todas las interfaces del sistema anteriores a C89 y lógicamente deberían hacerlo (como gettimeofday) usan punteros en su lugar, y la gente lo toma como un ejemplo.
zwol

Respuestas:


202

Para estructuras pequeñas (por ejemplo, punto, rect), pasar por valor es perfectamente aceptable. Pero, aparte de la velocidad, hay otra razón por la que debe tener cuidado al pasar / devolver grandes estructuras por valor: espacio de pila.

Una gran cantidad de programación en C es para sistemas integrados, donde la memoria es muy importante y los tamaños de pila pueden medirse en KB o incluso Bytes ... Si pasa o devuelve estructuras por valor, se colocarán copias de esas estructuras en la pila, lo que puede causar la situación de que este sitio lleva el nombre ...

Si veo una aplicación que parece tener un uso excesivo de la pila, las estructuras pasadas por valor son una de las cosas que busco primero.


2
"Si pasa o devuelve estructuras por valor, se colocarán copias de esas estructuras en la pila" Llamaría a braindead cualquier cadena de herramientas que lo haga. Sí, es triste que muchos lo hagan, pero no es algo que el estándar C requiera. Un compilador sensato lo optimizará todo.
Restablece a Mónica

1
@KubaOber Esta es la razón por la que eso no se hace a menudo: stackoverflow.com/questions/552134/…
Roddy

1
¿Existe una línea definitiva que separe una estructura pequeña de una estructura grande?
Josie Thompson el

63

Una razón para no hacer esto que no se ha mencionado es que esto puede causar un problema donde la compatibilidad binaria es importante.

Dependiendo del compilador utilizado, las estructuras se pueden pasar a través de la pila o registros dependiendo de las opciones / implementación del compilador

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

-fpcc-struct-return

-freg-struct-return

Si dos compiladores no están de acuerdo, las cosas pueden explotar. Huelga decir que las principales razones para no hacerlo están ilustradas son el consumo de pila y las razones de rendimiento.


44
Este era el tipo de respuesta que estaba buscando.
dkagedal

2
Es cierto, pero esas opciones no se relacionan con el paso por valor. se relacionan con estructuras que regresan, lo cual es algo completamente diferente. Devolver cosas por referencia suele ser una manera segura de dispararte con los dos pies. int &bar() { int f; int &j(f); return j;};
Roddy

19

Para responder realmente a esta pregunta, uno necesita profundizar en el terreno de la asamblea:

(El siguiente ejemplo usa gcc en x86_64. Cualquiera puede agregar otras arquitecturas como MSVC, ARM, etc.)

Tengamos nuestro programa de ejemplo:

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

Compílalo con optimizaciones completas

gcc -Wall -O3 foo.c -o foo

Mira la asamblea:

objdump -d foo | vim -

Esto es lo que obtenemos:

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)

Excluyendo los noplpads, give_two_doubles()tiene 27 bytes mientras que give_point()tiene 29 bytes. Por otra parte,give_point() produce una instrucción menos quegive_two_doubles()

Lo interesante es que notamos que el compilador ha podido optimizar moven las variantes SSE2 más rápidas movapdy movsd. Además, en give_two_doubles()realidad mueve datos dentro y fuera de la memoria, lo que hace que las cosas sean más lentas.

Aparentemente, gran parte de esto puede no ser aplicable en entornos integrados (que es donde el campo de juego para C es la mayor parte del tiempo hoy en día). No soy un asistente de ensamblaje, por lo que cualquier comentario sería bienvenido.


66
Contar el número de instrucciones no es tan interesante, a menos que pueda mostrar una gran diferencia, o contar aspectos más interesantes como el número de saltos difíciles de predecir, etc. Las propiedades de rendimiento reales son mucho más sutiles que el recuento de instrucciones .
dkagedal

66
@dkagedal: cierto. En retrospectiva, creo que mi propia respuesta fue escrita muy mal. Aunque no me concentré mucho en el número de instrucciones (no sé qué le dio esa impresión: P), el punto real que se debe hacer es que pasar estructura por valor es preferible a pasar por referencia para tipos pequeños. De todos modos, se prefiere pasar por valor porque es más simple (sin malabarismos de por vida, no hay que preocuparse de que alguien cambie sus datos o consttodo el tiempo) y descubrí que no hay mucha penalización de rendimiento (si no ganancia) en la copia de paso por valor , contrario a lo que muchos puedan creer.
kizzx2

15

La solución simple será devolver un código de error como valor de retorno y todo lo demás como un parámetro en la función.
Este parámetro puede ser una estructura, por supuesto, pero no ve ninguna ventaja particular que pase esto por valor, solo envía un puntero.
Pasar la estructura por valor es peligroso, debe tener mucho cuidado con lo que está pasando, recuerde que no hay un constructor de copia en C, si uno de los parámetros de la estructura es un puntero, el valor del puntero se copiará, puede ser muy confuso y difícil de entender. mantener.

Solo para completar la respuesta (crédito total a Roddy ), el uso de la pila es otra razón por la que no se pasa la estructura por valor, créanme, depurar el desbordamiento de la pila es PITA real.

Repetir para comentar:

Al pasar la estructura por el puntero, significa que alguna entidad es propietaria de este objeto y tiene un conocimiento completo de qué y cuándo se debe liberar. Pasar struct por valor crea referencias ocultas a los datos internos de struct (punteros a otras estructuras, etc.) en este punto es difícil de mantener (posible pero ¿por qué?).


66
Pero pasar un puntero no es más "peligroso" simplemente porque lo pones en una estructura, así que no lo compro.
dkagedal

Gran punto al copiar una estructura que contiene un puntero. Este punto puede no ser muy obvio. Para aquellos que no saben a qué se refiere, haga una búsqueda en copia profunda vs copia superficial.
zooropa

1
Una de las convenciones de la función C es que los parámetros de salida se enumeren primero antes de los parámetros de entrada, por ejemplo, int func (char * out, char * in);
zooropa

¿Quiere decir cómo, por ejemplo, getaddrinfo () coloca el parámetro de salida en último lugar? :-) Hay miles de convenciones y puedes elegir lo que quieras.
dkagedal

10

¡Una cosa que la gente aquí ha olvidado mencionar hasta ahora (o lo pasé por alto) es que las estructuras generalmente tienen un relleno!

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

Cada char tiene 1 byte, cada short tiene 2 bytes. ¿Qué tan grande es la estructura? No, no son 6 bytes. Al menos no en los sistemas más utilizados. En la mayoría de los sistemas será 8. El problema es que la alineación no es constante, depende del sistema, por lo que la misma estructura tendrá diferentes alineaciones y diferentes tamaños en diferentes sistemas.

No solo ese relleno consumirá aún más su pila, sino que también agrega la incertidumbre de no poder predecir el relleno con anticipación, a menos que sepa cómo su sistema se rellena y luego mira cada estructura que tiene en su aplicación y calcula el tamaño para ello. Pasar un puntero requiere una cantidad de espacio predecible, no hay incertidumbre. El tamaño de un puntero es conocido para el sistema, siempre es igual, independientemente de cómo se vea la estructura y los tamaños de puntero siempre se eligen de forma que estén alineados y no necesiten relleno.


2
Sí, pero el relleno existe sin depender de pasar la estructura por valor o por referencia.
Ilya

2
@dkagedal: ¿Qué parte de "diferentes tamaños en diferentes sistemas" no entendiste? Solo porque es así en su sistema, usted asume que debe ser igual para cualquier otro, es exactamente por eso que no debe pasar por valor. Se modificó la muestra para que también falle en su sistema.
Mecki

2
Creo que los comentarios de Mecki sobre el relleno de estructura son relevantes especialmente para sistemas embebidos donde el tamaño de la pila puede ser un problema.
zooropa

1
Supongo que el otro lado del argumento es que si su estructura es una estructura simple (que contiene un par de tipos primitivos), pasar por valor permitirá al compilador hacer malabarismos con registros, mientras que si usa punteros, las cosas terminarán en la memoria, que es más lenta. Eso tiene un nivel bastante bajo y depende en gran medida de su arquitectura de destino, si alguno de estos datos importa.
kizzx2

1
A menos que su estructura sea pequeña o su CPU tenga muchos registros (y las CPU Intel no), los datos terminan en la pila y eso también es memoria y es tan rápido / lento como cualquier otra memoria. Por otro lado, un puntero siempre es pequeño y solo un puntero y el puntero en sí mismo generalmente siempre terminará en un registro cuando se usa con más frecuencia.
Mecki

9

Creo que su pregunta ha resumido las cosas bastante bien.

Otra ventaja de pasar estructuras por valor es que la propiedad de la memoria es explícita. No se pregunta si la estructura es del montón y quién tiene la responsabilidad de liberarla.


9

Diría que pasar estructuras (no demasiado grandes) por valor, tanto como parámetros como valores de retorno, es una técnica perfectamente legítima. Hay que tener cuidado, por supuesto, de que la estructura sea del tipo POD o que la semántica de copia esté bien especificada.

Actualización: Lo siento, tenía mi gorra de pensamiento C ++. Recuerdo un momento en que no era legal en C devolver una estructura de una función, pero esto probablemente ha cambiado desde entonces. Todavía diría que es válido siempre y cuando todos los compiladores que esperas usar admitan la práctica.


Tenga en cuenta que mi pregunta era sobre C, no sobre C ++.
dkagedal

Es válido devolver la estructura de la función simplemente no es útil :)
Ilya

1
Me gusta la sugerencia de llya de usar la devolución como un código de error y parámetros para devolver datos de la función.
zooropa

8

Aquí hay algo que nadie mencionó:

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
}

Los miembros de a const structson const, pero si ese miembro es un puntero (me gusta char *), se convierte en char *constlugar de lo const char *que realmente queremos. Por supuesto, podríamos suponer que elconst trata de documentación de intenciones, y que cualquiera que viole esto está escribiendo un código incorrecto (que son), pero eso no es lo suficientemente bueno para algunos (especialmente aquellos que solo pasaron cuatro horas buscando la causa de un problema). choque).

La alternativa podría ser hacer struct const_blob { const char *c; size_t l }y usar eso, pero eso es bastante desordenado: entra en el mismo problema de esquema de nombres que tengo con typedeflos punteros. Por lo tanto, la mayoría de las personas se limitan a tener dos parámetros (o, más probablemente para este caso, usar una biblioteca de cadenas).


Sí, es perfectamente legal, y también es algo que quieres hacer a veces. Pero estoy de acuerdo en que es una limitación de la solución de estructura que no se pueden hacer los punteros que señalan para señalar const.
dkagedal

Un problema desagradable con la struct const_blobsolución es que incluso si const_blobtiene miembros que difieren de blobsolo en "constidad indirecta", los tipos struct blob*a a struct const_blob*se considerarán distintos para fines de una estricta regla de alias. En consecuencia, si el código convierte a blob*a a const_blob*, cualquier escritura posterior en la estructura subyacente utilizando un tipo invalidará silenciosamente cualquier puntero existente del otro tipo, de modo que cualquier uso invocará Comportamiento indefinido (que generalmente puede ser inofensivo, pero podría ser mortal) .
supercat

5

La página 150 del Tutorial de ensamblaje de PC en http://www.drpaulcarter.com/pcasm/ tiene una explicación clara sobre cómo C permite que una función devuelva una estructura:

C también permite utilizar un tipo de estructura como valor de retorno de una función. Obviamente, una estructura no puede ser devuelta en el registro EAX. Diferentes compiladores manejan esta situación de manera diferente. Una solución común que usan los compiladores es reescribir internamente la función como una que toma un puntero de estructura como parámetro. El puntero se usa para poner el valor de retorno en una estructura definida fuera de la rutina llamada.

Utilizo el siguiente código C para verificar la declaración anterior:

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 generar el ensamblaje de este fragmento 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

La pila antes de crear la llamada:

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

La pila justo después de llamar a crear:

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

2
Hay dos problemas aquí. La más obvia es que esto no describe en absoluto "cómo C permite que una función devuelva una estructura". Esto solo describe cómo se puede hacer en hardware x86 de 32 bits, que resulta ser una de las arquitecturas más limitadas cuando se observa el número de registros, etc. El segundo problema es la forma en que los compiladores de C generan código para devolver valores es dictado por el ABI (excepto para funciones no exportadas o en línea). Y, por cierto, las funciones en línea son probablemente uno de los lugares donde las estructuras de retorno son más útiles.
dkagedal

Gracias por las correcciones. Para una convención detallada completa de llamadas, en.wikipedia.org/wiki/Calling_convention es una buena referencia.
Jingguo Yao

@dkagedal: Lo importante no es solo que x86 haga las cosas de esta manera, sino que existe un enfoque "universal" (es decir, este) que permitiría a los compiladores de cualquier plataforma admitir devoluciones de cualquier tipo de estructura que no sea t tan grande como para volar la pila. Si bien los compiladores para muchas plataformas utilizarán otros medios más eficientes para manejar algunos valores de retorno de tipo estructura, no es necesario que el lenguaje limite los tipos de retorno de estructura a aquellos que la plataforma puede manejar de manera óptima.
supercat

0

Solo quiero señalar una ventaja de pasar sus estructuras por valor es que un compilador optimizador puede optimizar mejor su código.

Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.