Comment les compilateurs C implémentent-ils des fonctions renvoyant de grandes structures?

La valeur de retour d’une fonction est généralement stockée sur la stack ou dans un registre. Mais pour une grande structure, il faut que ce soit sur la stack. Combien faut-il copier dans un vrai compilateur pour ce code? Ou est-ce optimisé?

Par exemple:

struct Data { unsigned values[256]; }; Data createData() { Data data; // initialize data values... return data; } 

(En supposant que la fonction ne peut pas être en ligne ..)

Aucun; aucune copie n’est faite.

L’adresse de la valeur de retour des données de l’appelant est en fait transmise à la fonction en tant qu’argument masqué, et la fonction createData écrit simplement dans le cadre de stack de l’appelant.

Ceci est connu sous le nom d’optimisation de la valeur de retour nommée . Voir également la FAQ c ++ sur ce sujet .

Les compilateurs C ++ de qualité commerciale implémentent le retour par valeur d’une manière qui leur permet d’éliminer les frais généraux, du moins dans les cas simples

Lorsque votreCode () appelle rbv (), le compilateur passe secrètement un pointeur sur l’emplacement où rbv () est censé construire l’object “renvoyé”.

Vous pouvez démontrer que cela a été fait en ajoutant un destructeur avec un printf à votre structure. Le destructeur ne doit être appelé qu’une seule fois si cette optimisation retour par valeur est en cours, sinon deux fois.

Vous pouvez aussi vérifier l’assemblage pour voir que cela se produit:

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

voici le assembly:

 __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: 

Curieusement, il a alloué suffisamment d’espace sur la stack pour l’élément de données subl $1032, %esp , mais notez qu’il prend le premier argument de la stack 8(%ebp) comme adresse de base de l’object, puis initialise l’élément 6 de celui-ci. article. Comme nous n’avons spécifié aucun argument pour createData, ceci est curieux jusqu’à ce que vous réalisiez que c’est le pointeur caché secret vers la version de Data du parent.

Mais pour une grande structure, il doit être sur la stack de tas .

En effet oui! Une grande structure déclarée comme variable locale est allouée sur la stack. Heureux de l’avoir éclairci.

Pour éviter de copier, comme d’autres l’ont noté:

  • La plupart des conventions d’appel traitent de “fonction retournant une structure” en passant un paramètre supplémentaire qui pointe l’emplacement dans le cadre de stack de l’appelant dans lequel la structure doit être placée. C’est une question qui relève de la convention d’appel et non de la langue.

  • Avec cette convention d’appel, il devient possible, même pour un compilateur relativement simple, de constater quand un chemin de code va définitivement retourner une structure, et de fixer les affectations aux membres de cette structure afin qu’ils soient directement insérés dans le cadre de l’appelant t doivent être copiés. La clé est que le compilateur remarque que tous les chemins de code de terminaison via la fonction retournent la même variable struct. Si tel est le cas, le compilateur peut utiliser en toute sécurité l’espace situé dans le cadre de l’appelant, éliminant ainsi le besoin d’une copie au sharepoint retour.

Il y a beaucoup d’exemples donnés, mais fondamentalement

Cette question n’a pas de réponse définitive. cela dépendra du compilateur.

C ne spécifie pas la taille des structures renvoyées par une fonction.

Voici quelques tests pour un compilateur particulier, gcc 4.1.2 sur x86 RHEL 5.4

gcc cas sortingvial, pas de copie

 [00:05:21 1 ~] $ gcc -O2 -S -c tc [00:05:23 1 ~] $ cat ts .file "tc" .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 cas plus réaliste, allouer sur stack, mémoire à l’appelant

 #include  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 tc [00:06:10 1 ~] $ cat ts .file "tc" .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 ### a beaucoup grandi et ne copie pas pour le cas ci-dessus non sortingvial.

  .file "tc" .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 

De plus, VS2008 (compilé ci-dessus en C) réservera les données de structure sur la stack de createData () et effectuera une boucle rep movsd pour les recopier dans l'appelant en mode débogage. En mode libération, la valeur renvoyée par rand sera déplacée. () (% eax) directement à l'appelant

 typedef struct { unsigned value[256]; } Data; Data createData(void) { Data r; calcualte(&r); return r; } Data d = createData(); 

msvc (6,8,9) et gcc mingw (3.4.5,4.4.0) générera un code similaire au pseudocode suivant

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

gcc sur linux émettra un memcpy () pour copier la structure sur la stack de l’appelant. Si la fonction dispose d’une liaison interne, d’autres optimisations deviennent disponibles.