Une structure peut être passée/renvoyée par valeur ou passée/retournée par référence (via un pointeur) en C.
Le consensus général semble être que le premier peut être appliqué à de petites structures sans pénalité dans la plupart des cas. Voir Y a-t-il un cas pour lequel le retour direct d'une structure est une bonne pratique? et Y a-t-il des inconvénients à passer des structures par valeur en C, plutôt que de passer un pointeur?
Et qu'éviter une déréférence peut être bénéfique à la fois du point de vue de la vitesse et de la clarté. Mais qu'est-ce qui compte comme petit? Je pense que nous pouvons tous convenir qu'il s'agit d'une petite structure:
struct Point { int x, y; };
Que l'on peut passer par la valeur en toute impunité:
struct Point sum(struct Point a, struct Point b) {
return struct Point { .x = a.x + b.x, .y = a.y + b.y };
}
Et que Linux task_struct
est une grande structure:
Que nous voudrions éviter de mettre la pile à tout prix (surtout avec ces piles en mode noyau 8K!). Mais qu'en est-il des intermédiaires? Je suppose que les structures plus petites qu'un registre sont correctes. Mais qu'en est-il?
typedef struct _mx_node_t mx_node_t;
typedef struct _mx_Edge_t mx_Edge_t;
struct _mx_Edge_t {
char symbol;
size_t next;
};
struct _mx_node_t {
size_t id;
mx_Edge_t Edge[2];
int action;
};
Quelle est la meilleure règle empirique pour déterminer si une structure est suffisamment petite pour pouvoir être transmise en toute sécurité (à moins de circonstances atténuantes telles que certaines récursion profonde)?
Enfin, ne me dites pas que j'ai besoin de profil. Je demande une heuristique à utiliser lorsque je suis trop paresseux/cela ne vaut pas la peine d'enquêter davantage.
EDIT: J'ai deux questions de suivi basées sur les réponses jusqu'à présent:
Et si la structure est en fait plus petite qu'un pointeur vers elle?
Que faire si une copie superficielle est le comportement souhaité (la fonction appelée effectuera de toute façon une copie superficielle)?
EDIT: Je ne sais pas pourquoi cela a été marqué comme un double possible, car je lie en fait l'autre question dans ma question. Je demande des précisions sur ce qui constitue une structure petite et je suis bien conscient que la plupart du temps, les structures doivent être transmises par référence.
Sur les petites architectures intégrées (8/16 bits) - toujours passez par pointeur, car les structures non triviales ne rentrent pas dans ces registres minuscules, et ces machines sont généralement également dépourvues de registre.
Sur les architectures de type PC (processeurs 32 et 64 bits) - le passage d'une structure par valeur est OK à condition que sizeof(mystruct_t) <= 2*sizeof(mystruct_t*)
et la fonction n'ait pas beaucoup (généralement plus de 3 mots machine) d'autres arguments. Dans ces circonstances, un compilateur d'optimisation typique transmet/renvoie la structure dans un registre ou une paire de registres. Cependant, sur x86-32, ce conseil doit être pris avec un gros grain de sel, en raison de la pression de registre extraordinaire qu'un compilateur x86-32 doit gérer - le passage d'un pointeur peut encore être plus rapide en raison de la réduction du déversement et du remplissage du registre.
Le retour d'une structure par valeur sur des PC-likes, en revanche, suit la même règle, sauf que lorsqu'une structure est retournée par un pointeur, la structure à remplir doit être passée par pointeur également - sinon, l'appelé et l'appelant sont coincés devant s'accorder sur la façon de gérer la mémoire de cette structure.
Mon expérience, près de 40 ans d'intégration en temps réel, dont 20 en C; est que le meilleur moyen est de passer un pointeur.
Dans les deux cas, l'adresse de la structure doit être chargée, puis l'offset pour le champ d'intérêt doit être calculé ...
Lors du passage de la structure entière, si elle n'est pas passée par référence, alors
Des considérations similaires existent quand une structure est renvoyée par valeur.
Cependant, de "petites" structures, qui peuvent être complètement conservées dans un registre de travail à deux, sont passées dans ces registres, surtout si certains niveaux d'optimisation sont utilisés dans l'instruction de compilation.
Les détails de ce qui est considéré comme "petit" dépendent du compilateur et de l'architecture matérielle sous-jacente.
Étant donné que la partie qui passe l'argument de la question est déjà répondue, je vais me concentrer sur la partie de retour.
La meilleure chose à faire IMO est de ne pas retourner du tout de structures ou de pointeurs vers des structures, mais de passer un pointeur vers la 'structure de résultat' vers la fonction.
void sum(struct Point* result, struct Point* a, struct Point* b);
Cela présente les avantages suivants:
result
peut vivre sur la pile ou sur le tas, à la discrétion de l'appelant.La façon dont une structure est transmise à ou à partir d'une fonction dépend de l'interface binaire d'application (ABI) et de la norme d'appel de procédure (PCS, parfois incluse dans l'ABI) pour votre plate-forme cible (CPU/OS, pour certaines plates-formes, il peut y avoir plus de une version).
Si le PCS permet en fait de passer une structure dans les registres, cela dépend non seulement de sa taille, mais aussi de sa position dans la liste des arguments et des types d'arguments précédents. ARM-PCS (AAPCS), par exemple, emballe les arguments dans les 4 premiers registres jusqu'à ce qu'ils soient pleins et transmette d'autres données à la pile, même si cela signifie qu'un argument est divisé (tout simplifié, si vous êtes intéressé: les documents sont téléchargeables gratuitement à partir d'ARM ).
Pour les structures retournées, si elles ne sont pas passées par les registres, la plupart des PCS allouent l'espace sur la pile par l'appelant et passent un pointeur vers la structure à l'appelé (variante implicite). Ceci est identique à une variable locale dans l'appelant et en passant explicitement le pointeur - pour l'appelé. Cependant, pour la variante implicite, le résultat doit être copié dans une autre structure, car il n'y a aucun moyen d'obtenir une référence à la structure allouée implicitement.
Certains PCS peuvent faire de même pour les structures d'arguments, d'autres utilisent simplement les mêmes mécanismes que pour les scalaires. De toute façon, vous différez ces optimisations jusqu'à ce que vous sachiez vraiment que vous en avez besoin. Lisez également le PCS de votre plateforme cible. N'oubliez pas que votre code peut être encore pire sur une autre plateforme.
Remarque: passer un struct à travers un temp global n'est pas utilisé par PCS moderne, car il n'est pas thread-safe. Pour certaines petites architectures de microcontrôleurs, cela peut cependant être différent. Surtout s'ils n'ont qu'une petite pile (S08) ou des fonctionnalités restreintes (PIC). Mais pour la plupart du temps, les structures ne sont pas passées dans les registres non plus, et le passage par pointeur est fortement recommandé.
Si c'est juste pour l'immuabilité de l'original: passez un const mystruct *ptr
. À moins que vous ne supprimiez le const
qui donnera un avertissement au moins lors de l'écriture dans la structure. Le pointeur lui-même peut également être constant: const mystruct * const ptr
.
Donc: Aucune règle d'or; cela dépend de trop de facteurs.
Vraiment, la meilleure règle de base, quand il s'agit de passer un struct comme argument à une fonction par référence vs par valeur, est d'éviter de le passer par valeur. Les risques l'emportent presque toujours sur les avantages.
Par souci d'exhaustivité, je soulignerai que lors du passage/retour d'une structure par valeur, quelques choses se produisent:
Passons maintenant à ce que signifie assez petit en termes de taille de la structure - pour que cela `` vaille '' de le passer en valeur, cela dépendra de quelques choses:
Bottom line - il est très difficile de dire quand il est correct de passer une structure par valeur. Il est plus sûr de ne pas le faire :)
Remarque: les raisons de le faire dans un sens ou dans l'autre se chevauchent.
Quand passer/retourner par valeur:
int
, double
, pointeur.L'objet est conceptuellement un petit numérique
struct quaternion {
long double i,j,k;
}
struct pixel {
uint16_t r,g,b;
}
struct money {
intmax_t;
int exponent;
}
Quand utiliser un pointeur sur l'objet
L'objet a besoin de la gestion de la mémoire.
struct mystring {
char *s;
size_t length;
size_t size;
}
Notes: Rappelons qu'en C, rien n'est vraiment passé par référence. Même le passage d'un pointeur est transmis par valeur, car la valeur du pointeur est copiée et transmise.
Je préfère passer des nombres, que ce soit int
ou pixel
par valeur car il est conceptuellement plus facile de comprendre le code. Le passage des chiffres par adresse est conceptuel un peu plus difficile. Avec des objets numériques plus grands, il peut être plus rapide de passer par adresse.
Les objets dont l'adresse a été transmise peuvent utiliser restrict
pour informer la fonction que les objets ne se chevauchent pas.
Sur un PC typique, les performances ne devraient pas être un problème, même pour des structures assez grandes (plusieurs dizaines d'octets). Par conséquent, d'autres critères sont importants, en particulier la sémantique: vous souhaitez en effet travailler sur une copie? Ou sur le même objet, par ex. lors de la manipulation de listes liées? La ligne directrice devrait être d'exprimer la sémantique souhaitée avec la construction de langage la plus appropriée afin de rendre le code lisible et maintenable.
Cela dit, s'il y a un impact sur les performances, il peut ne pas être aussi clair qu'on pourrait le penser.
Memcpy est rapide et la localité de la mémoire (ce qui est bon pour la pile) peut être plus importante que la taille des données: la copie peut toutes se produire dans le cache, si vous passez et renvoyez une structure par valeur sur la pile. De plus, l'optimisation de la valeur de retour devrait éviter la copie redondante des variables locales à renvoyer (ce que les compilateurs naïfs ont fait il y a 20 ou 30 ans).
Passer des pointeurs autour introduit des alias aux emplacements de mémoire qui ne peuvent plus être mis en cache plus efficacement. Les langages modernes sont souvent plus axés sur la valeur car toutes les données sont isolées des effets secondaires, ce qui améliore la capacité d'optimisation du compilateur.
Le résultat est oui, sauf si vous rencontrez des problèmes, n'hésitez pas à passer par la valeur si cela est plus pratique ou approprié. Cela peut même être plus rapide.