¿Cómo implementan los compiladores de C funciones que devuelven estructuras grandes?

8 minutos de lectura

avatar de usuario
steve hanov

El valor de retorno de una función generalmente se almacena en la pila o en un registro. Pero para una estructura grande, tiene que estar en la pila. ¿Cuánta copia tiene que ocurrir en un compilador real para este código? ¿O está optimizado?

Por ejemplo:

struct Data {
    unsigned values[256];
};

Data createData() 
{
    Data data;
    // initialize data values...
    return data;
}

(Suponiendo que la función no se puede alinear…)

  • Avísame si mi edición no fue lo que pretendías.

    – danben

    28 de enero de 2010 a las 15:44

  • Ese es realmente un mal ejemplo, ya que “datos” es esencialmente un sinónimo de “&datos”.[0]”. Los punteros son escalares.

    – TED

    28 de enero de 2010 a las 16:04

  • Prefiero empujar esto al programador en lugar del compilador. Los objetos grandes siempre deben pasarse con el puntero. Esto muestra explícitamente que el objeto grande no se copia.

    – Thomas Matthews

    28 de enero de 2010 a las 19:21

avatar de usuario
Alex Brown

Ninguna; no se hacen copias.

La dirección del valor de retorno de datos de la persona que llama en realidad se pasa como un argumento oculto a la función, y la función createData simplemente escribe en el marco de la pila de la persona que llama.

Esto se conoce como el optimización del valor de retorno nombrado. También vea el c ++ preguntas frecuentes sobre este tema.

Los compiladores de C++ de grado comercial implementan el retorno por valor de una manera que les permite eliminar la sobrecarga, al menos en casos simples.

Cuando yourCode() llama a rbv(), el compilador secretamente pasa un puntero a la ubicación donde se supone que rbv() debe construir el objeto “devuelto”.

Puede demostrar que esto se ha hecho agregando un destructor con un printf a su estructura. El destructor solo debe llamarse una vez si esta optimización de devolución por valor está en funcionamiento, de lo contrario, dos veces.

También puede verificar el ensamblaje para ver que esto sucede:

Data createData() 
{
    Data data;
    // initialize data values...
    data.values[5] = 6;
    return data;
}

aquí está la asamblea:

__Z10createDatav:
LFB2:
        pushl   %ebp
LCFI0:
        movl    %esp, %ebp
LCFI1:
        subl    $1032, %esp
LCFI2:
        movl    8(%ebp), %eax
        movl    $6, 20(%eax)
        leave
        ret     $4
LFE2:

Curiosamente, asignó suficiente espacio en la pila para el elemento de datos subl $1032, %esppero tenga en cuenta que toma el primer argumento en la pila 8(%ebp) como la dirección base del objeto y luego inicializa el elemento 6 de ese elemento. Dado que no especificamos ningún argumento para createData, esto es curioso hasta que te das cuenta de que este es el puntero secreto oculto a la versión principal de Data.

  • “no copiar” es exagerar un poco esto. la estructura todavía tiene que ser copiada. incluso si “createData ()” solo modificó 1 elemento en la estructura de datos, la estructura completa aún se copia de nuevo a la persona que llama una vez; esto se verifica fácilmente al observar el ensamblaje generado. (Al menos para funciones no estáticas que necesitan adherirse a una plataforma ABI. Hay más optimizaciones disponibles para funciones estáticas, u optimizadores de programas completos para los compiladores que las tienen.

    – Anónimo

    28 de enero de 2010 a las 16:03


  • en serio, hay sin copiar. He comprobado el ensamblado generado.

    – Alex Marrón

    28 de enero de 2010 a las 16:23

  • @all: digamos que depende. Porque eso depende. El caso simple anterior está optimizado, todo se conoce en tiempo de compilación. Intente completar los 256 valores en un bucle, con, por ejemplo, data.values[i] = al azar(). o alguna otra función externa. gcc (4.4.1 al menos con -O2) memcpy todo de vuelta a la persona que llama. Sin embargo, gcc no generará un memcpy para el caso simple anterior. Otros compiladores pueden diferir.

    – Leeroy

    28 de enero de 2010 a las 22:31


  • más información, la pregunta fue etiquetada como C. gcc de hecho no generará una copia para los retornos de estructura si compila el código como C++. (Pero voluntad si lo compila como C si todos los valores de la matriz se completan desde fuentes desconocidas en el momento de la compilación)

    – Leeroy

    28 de enero de 2010 a las 22:42


  • la pregunta fue etiquetada como C, pero el código es claramente c++. Tenga en cuenta la estructura que falta antes de cada uso del tipo de datos.

    – Alex Marrón

    29 de enero de 2010 a las 12:02

avatar de usuario
norman ramsey

Pero para una estructura grande, tiene que estar en el montón apilar.

¡Claro que sí! Una estructura grande declarada como variable local se asigna en la pila. Me alegro de haberlo aclarado.

En cuanto a evitar copiar, como han señalado otros:

  • La mayoría de las convenciones de llamada se ocupan de la “estructura de devolución de función” al pasar un parámetro adicional que señala la ubicación en el marco de la pila de la persona que llama en la que se debe colocar la estructura. Este es definitivamente un asunto de la convención de llamadas y no del idioma.

  • Con esta convención de llamada, es posible que incluso un compilador relativamente simple se dé cuenta cuando una ruta de código definitivamente va a devolver una estructura, y que corrija las asignaciones a los miembros de esa estructura para que vayan directamente al marco de la persona que llama y no t tiene que ser copiado. La clave es que el compilador se dé cuenta de que todos las rutas de código de terminación a través de la función devuelven el mismo variable de estructura. Si ese es el caso, el compilador puede usar con seguridad el espacio en el marco de la persona que llama, eliminando la necesidad de una copia en el punto de retorno.

  • En realidad, existen dos convenciones de direcciones de pases de llamadas diferentes, dependiendo de si se requiere que el método llamado asegure que la estructura pasada no se escriba hasta después de que hayan ocurrido todos los efectos secundarios, o si se requiere que la persona que llama se asegure de que el contenido de la estructura pasada no será visible en ninguna parte hasta que el método regrese. Cada uno puede ser ventajoso en algunas condiciones.

    – Super gato

    10 de febrero de 2014 a las 18:05

Hay muchos ejemplos dados, pero básicamente

Esta pregunta no tiene una respuesta definitiva. Dependerá del compilador.

C no especifica qué tan grandes se devuelven las estructuras de una función.

Aquí hay algunas pruebas para un compilador en particular, gcc 4.1.2 en x86 RHEL 5.4

gcc caso trivial, sin copiar

[00:05:21 1 ~] $ gcc -O2 -S -c t.c
[00:05:23 1 ~] $ cat t.s
        .file   "t.c"
        .text
        .p2align 4,,15
.globl createData
        .type   createData, @function
createData:
        pushl   %ebp
        movl    %esp, %ebp
        movl    8(%ebp), %eax
        movl    $1, 24(%eax)
        popl    %ebp
        ret     $4
        .size   createData, .-createData
        .ident  "GCC: (GNU) 4.1.2 20080704 (Red Hat 4.1.2-46)"
        .section        .note.GNU-stack,"",@progbits

gcc caso más realista, asignar en la pila, memcpy a la persona que llama

#include <stdlib.h>
struct Data {
    unsigned values[256];
};
struct Data createData()
{
    struct Data data;
    int i;
    for(i = 0; i < 256 ; i++)
        data.values[i] = rand();
    return data;
}

[00:06:08 1 ~] $ gcc -O2 -S -c t.c
[00:06:10 1 ~] $ cat t.s
        .file   "t.c"
        .text
        .p2align 4,,15
.globl createData
        .type   createData, @function
createData:
        pushl   %ebp
        movl    %esp, %ebp
        pushl   %edi
        pushl   %esi
        pushl   %ebx
        movl    $1, %ebx
        subl    $1036, %esp
        movl    8(%ebp), %edi
        leal    -1036(%ebp), %esi
        .p2align 4,,7
.L2:
        call    rand
        movl    %eax, -4(%esi,%ebx,4)
        addl    $1, %ebx
        cmpl    $257, %ebx
        jne     .L2
        movl    %esi, 4(%esp)
        movl    %edi, (%esp)
        movl    $1024, 8(%esp)
        call    memcpy
        addl    $1036, %esp
        movl    %edi, %eax
        popl    %ebx
        popl    %esi
        popl    %edi
        popl    %ebp
        ret     $4
        .size   createData, .-createData
        .ident  "GCC: (GNU) 4.1.2 20080704 (Red Hat 4.1.2-46)"
        .section        .note.GNU-stack,"",@progbits

gcc 4.4.2### ha crecido mucho y no se copia para el caso no trivial anterior.

        .file   "t.c"
        .text
        .p2align 4,,15
.globl createData
        .type   createData, @function
createData:
        pushl   %ebp
        movl    %esp, %ebp
        pushl   %edi
        pushl   %esi
        pushl   %ebx
        movl    $1, %ebx
        subl    $1036, %esp
        movl    8(%ebp), %edi
        leal    -1036(%ebp), %esi
        .p2align 4,,7
.L2:
        call    rand
        movl    %eax, -4(%esi,%ebx,4)
        addl    $1, %ebx
        cmpl    $257, %ebx
        jne     .L2
        movl    %esi, 4(%esp)
        movl    %edi, (%esp)
        movl    $1024, 8(%esp)
        call    memcpy
        addl    $1036, %esp
        movl    %edi, %eax
        popl    %ebx
        popl    %esi
        popl    %edi
        popl    %ebp
        ret     $4
        .size   createData, .-createData
        .ident  "GCC: (GNU) 4.1.2 20080704 (Red Hat 4.1.2-46)"
        .section        .note.GNU-stack,"",@progbits

Además, VS2008 (compilado lo anterior como C) reservará struct Data en la pila de createData() y hará una rep movsd bucle para volver a copiarlo a la persona que llama en el modo de depuración, en el modo de liberación moverá el valor de retorno de rand() (%eax) directamente de vuelta a la persona que llama

avatar de usuario
PropioWaterloo

typedef struct {
    unsigned value[256];
} Data;

Data createData(void) {
    Data r;
    calcualte(&r);
    return r;
}

Data d = createData();

msvc(6,8,9) y CCG mingw(3.4.5,4.4.0) generará código como el siguiente pseudocódigo

void createData(Data* r) {
      calculate(&r)
}
Data d;
createData(&d);

gcc en Linux emitirá un memcpy() para copiar la estructura en la pila de la persona que llama. Sin embargo, si la función tiene un enlace interno, habrá más optimizaciones disponibles.

¿Ha sido útil esta solución?

Esta web utiliza cookies propias y de terceros para su correcto funcionamiento y para fines analíticos y para mostrarte publicidad relacionada con sus preferencias en base a un perfil elaborado a partir de tus hábitos de navegación. Al hacer clic en el botón Aceptar, acepta el uso de estas tecnologías y el procesamiento de tus datos para estos propósitos. Configurar y más información
Privacidad