Existe-t-il des inconvénients à passer des structures par valeur en C, plutôt que de passer un pointeur?
Si la structure est grande, il y a évidemment l'aspect performanc de la copie de nombreuses données, mais pour une structure plus petite, il devrait en principe être identique à la transmission de plusieurs valeurs à une fonction.
C'est peut-être encore plus intéressant lorsqu'il est utilisé comme valeur de retour. C n'a que des valeurs de retour uniques à partir de fonctions, mais vous en avez souvent besoin de plusieurs. Donc, une solution simple consiste à les mettre dans une structure et à les renvoyer.
Y a-t-il des raisons pour ou contre cela?
Comme ce dont je parle ici n’est peut-être pas évident pour tout le monde, je vais donner un exemple simple.
Si vous programmez en C, vous commencerez tôt ou tard à écrire des fonctions qui ressemblent à ceci:
void examine_data(const char *ptr, size_t len)
{
...
}
char *p = ...;
size_t l = ...;
examine_data(p, l);
Ce n'est pas un problème. Le seul problème est que vous devez vous mettre d'accord avec votre collègue sur l'ordre des paramètres afin d'utiliser la même convention dans toutes les fonctions.
Mais que se passe-t-il lorsque vous souhaitez renvoyer le même type d'informations? Vous obtenez généralement quelque chose comme ça:
char *get_data(size_t *len);
{
...
*len = ...datalen...;
return ...data...;
}
size_t len;
char *p = get_data(&len);
Cela fonctionne bien, mais est beaucoup plus problématique. Une valeur de retour est une valeur de retour, sauf que dans cette implémentation, ce n'est pas le cas. Il n’existe aucun moyen de déduire de ce qui précède que la fonction get_data n’est pas autorisée à regarder ce à quoi pointe len. Et rien ne permet au compilateur de vérifier qu’une valeur est effectivement renvoyée via ce pointeur. Ainsi, le mois prochain, lorsque quelqu'un modifie le code sans le comprendre correctement (parce qu'il n'a pas lu la documentation?), Il est cassé sans que personne ne s'en aperçoive, ou il commence à planter de manière aléatoire.
Donc, la solution que je propose est la structure simple
struct blob { char *ptr; size_t len; }
Les exemples peuvent être réécrits comme ceci:
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();
Pour une raison quelconque, je pense que la plupart des gens voudraient instinctivement faire examiner_data un pointeur vers un blob struct, mais je ne vois pas pourquoi. Il y a toujours un pointeur et un entier, il est juste beaucoup plus clair qu'ils vont ensemble. Et dans le cas de get_data, il est impossible de gâcher la procédure décrite précédemment, car il n'y a pas de valeur d'entrée pour la longueur et il doit y avoir une longueur renvoyée.
Pour de petites structures (par exemple, point, rect), passer par valeur est parfaitement acceptable. Mais, mis à part la vitesse, il existe une autre raison pour laquelle vous devez faire attention de ne pas passer/renvoyer de grandes structures par valeur: Espace de pile.
Une grande partie de la programmation en C est destinée aux systèmes embarqués, où la mémoire est rare et les tailles de pile peuvent être mesurées en Ko ou même en octets ... Si vous passez ou retournez des structures par valeur, des copies de ces structures seront placées sur la pile, provoquant potentiellement la situation que ce site est nommé d'après ...
Si je vois une application qui semble utiliser excessivement la pile, les structs passés par valeur sont l’une des choses que je cherche en premier.
Une raison de ne pas le faire qui n’a pas été mentionnée est que cela peut poser un problème pour lequel la compatibilité binaire est importante.
Selon le compilateur utilisé, les structures peuvent être transmises via la pile ou des registres en fonction des options/de la mise en œuvre du compilateur
Voir: http://gcc.gnu.org/onlinedocs/gcc/Code-Gen-Options.html
-fpcc-struct-return
-freg-struct-return
Si deux compilateurs sont en désaccord, les choses peuvent exploser. Il va sans dire que les principales raisons de ne pas le faire sont illustrées par des raisons de consommation de pile et de performances.
Pour vraiment répondre à cette question, il faut creuser profondément dans le pays de l'Assemblée:
(L'exemple suivant utilise gcc sur x86_64. N'importe qui peut ajouter d'autres architectures telles que MSVC, ARM, etc.)
Prenons notre exemple de programme:
// 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;
}
Compilez-le avec des optimisations complètes
gcc -Wall -O3 foo.c -o foo
Regardez l'Assemblée:
objdump -d foo | vim -
Voici ce que nous obtenons:
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)
En excluant les pads nopl
, give_two_doubles()
a 27 octets tandis que give_point()
a 29 octets. Par contre, give_point()
donne une instruction de moins que give_two_doubles()
Ce qui est intéressant, c’est que nous remarquons que le compilateur a pu optimiser mov
dans les variantes SSE2 plus rapides movapd
et movsd
. De plus, give_two_doubles()
déplace les données de la mémoire, ce qui ralentit les choses.
Apparemment, une grande partie de ceci peut ne pas être applicable dans les environnements embarqués (c'est où le terrain de jeu pour C est la plupart du temps de nos jours). Je ne suis pas un assistant de l'Assemblée, tout commentaire serait le bienvenu!
La solution simple consiste à renvoyer un code d'erreur en tant que valeur de retour et tout le reste en tant que paramètre de la fonction.
Ce paramètre peut être une structure bien sûr, mais ne voit aucun avantage particulier à passer cela par valeur, mais juste un pointeur.
Passer structure par valeur est dangereux, vous devez faire très attention à ce que vous passez, souvenez-vous qu’il n’existe pas de constructeur de copie en C, si l’un des paramètres de structure est un pointeur, la valeur du pointeur sera copiée. très déroutant et difficile à maintenir.
Juste pour compléter la réponse (crédit complet à Roddy ), l’utilisation de la pile est une autre raison pour laquelle on ne passe pas la structure à la valeur, croyez-moi que le débordement de pile est un vrai PITA.
Replay to comment:
Passer struct par pointeur signifie qu'une entité a la propriété de cet objet et sait parfaitement quoi et quand doit être libérée. Passer struct par valeur crée des références cachées aux données internes de struct (pointeurs sur d'autres structures, etc.), ce qui est difficile à maintenir (possible mais pourquoi?).
Je dirais que passer des structures (pas trop grandes) par valeur, à la fois en tant que paramètres et en tant que valeurs de retour, est une technique parfaitement légitime. Bien sûr, il faut veiller à ce que la structure soit de type POD ou que la sémantique de la copie soit bien spécifiée.
Mise à jour: Désolé, j'avais ma casquette de réflexion C++. Je me souviens d'une époque où il n'était pas légal en C de renvoyer une structure à partir d'une fonction, mais cela a probablement changé depuis. Je dirais quand même que c'est valable tant que tous les compilateurs que vous comptez utiliser vont supporter cette pratique.
Je pense que votre question résume assez bien les choses.
Un autre avantage de passer des structures par valeur est que la propriété de la mémoire est explicite. Personne ne se demande si la structure provient du tas et qui a la responsabilité de la libérer.
Une chose que les gens ici ont oublié de mentionner jusqu’à présent (ou que j’ai négligée) est que les structures ont généralement un rembourrage!
struct {
short a;
char b;
short c;
char d;
}
Chaque caractère est 1 octet, chaque court métrage est 2 octets. Quelle est la taille de la structure? Non, ce n'est pas 6 octets. Du moins pas sur les systèmes les plus couramment utilisés. Le problème est que l'alignement n'est pas constant, il dépend du système, de sorte que la même structure aura un alignement différent et des tailles différentes sur des systèmes différents.
Non seulement ce rembourrage grignotera votre pile, il ajoutera également l’incertitude de ne pas pouvoir le prédire à l’avance, à moins que vous ne sachiez comment votre système compresse, puis examinez chaque structure de votre application et calculez sa taille. pour ça. Passer un pointeur prend une quantité d'espace prévisible - il n'y a pas d'incertitude. La taille d'un pointeur est connue du système. Elle est toujours la même, quelle que soit l'apparence de la structure. La taille des pointeurs est toujours choisie de manière à être alignée et à ne nécessiter aucun remplissage.
Voici quelque chose que personne n'a mentionné:
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
}
Membres d'un const struct
sont const
, mais si ce membre est un pointeur (comme char *
), il devient char *const
plûtot que le const char *
nous voulons vraiment. Bien sûr, nous pourrions supposer que const
est une documentation d'intention et que quiconque le viole écrit de mauvais code (ce qui est le cas), mais cela ne suffit pas pour certains (surtout ceux qui viennent de passer quatre heures traquer la cause d’un accident).
L'alternative pourrait être de faire un struct const_blob { const char *c; size_t l }
et utilisez-le, mais c’est un peu désordonné - cela pose le même problème de schéma de nommage que j’ai avec les pointeurs typedef
ing. Ainsi, la plupart des gens s'en tiennent à deux paramètres (ou, plus probablement, à une bibliothèque de chaînes).
La page 150 du didacticiel de PC Assembly sur http://www.drpaulcarter.com/pcasm/ explique clairement comment C permet à une fonction de renvoyer une struct:
C permet également d'utiliser un type de structure comme valeur de retour d'une fonction. De toute évidence, une structure ne peut pas être renvoyée dans le registre EAX. Différents compilateurs traitent cette situation différemment. Une solution courante utilisée par les compilateurs consiste à réécrire la fonction en interne en prenant pour paramètre un pointeur de structure. Le pointeur est utilisé pour placer la valeur de retour dans une structure définie en dehors de la routine appelée.
J'utilise le code C suivant pour vérifier la déclaration ci-dessus:
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;
}
Utilisez "gcc -S" pour générer l'assembly pour ce morceau de code 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 pile avant l'appel crée:
+---------------------------+
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 pile juste après l'appel crée:
+---------------------------+
| ebp-8 (address) |
+---------------------------+
| return address |
+---------------------------+
ebp,esp | saved ebp |
+---------------------------+
Je veux juste souligner un avantage de passer vos structures par valeur, c'est qu'un compilateur d'optimisation peut mieux optimiser votre code.