web-dev-qa-db-fra.com

Quand dois-je transmettre ou renvoyer une structure par valeur?

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:

https://github.com/torvalds/linux/blob/b953c0d234bc72e8489d3bf51a276c5c4ec85345/include/linux/sched.h#L1292-1727

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:

  1. Et si la structure est en fait plus petite qu'un pointeur vers elle?

  2. 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.

41
Kaiting Chen

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.

16
LThode

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

  1. il n'est pas placé sur la pile
  2. il est copié, généralement par un appel caché à memcpy ()
  3. il est copié dans une section de la mémoire qui est désormais "réservée" et indisponible pour toute autre partie du programme.

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.

23
user3629249

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

  • La structure result peut vivre sur la pile ou sur le tas, à la discrétion de l'appelant.
  • Il n'y a aucun problème de propriété, car il est clair que l'appelant est responsable de l'allocation et de la libération de la structure de résultat.
  • La structure pourrait même être plus longue que ce qui est nécessaire, ou être intégrée dans une structure plus grande.
7
alain

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:

  1. tous les membres de la structure sont copiés sur la pile
  2. si vous retournez une structure par valeur, encore une fois, tous les membres sont copiés de la mémoire de pile de la fonction vers un nouvel emplacement de mémoire.
  3. l'opération est sujette aux erreurs - si les membres de la structure sont des pointeurs, une erreur courante est de supposer que vous pouvez passer le paramètre en toute sécurité, car vous utilisez des pointeurs - cela peut entraîner des bogues très difficiles à repérer.
  4. si votre fonction modifie la valeur des paramètres d'entrée et que vos entrées sont des variables struct, passées par valeur, vous devez vous rappeler de TOUJOURS renvoyer une variable struct par valeur (j'ai vu celle-ci plusieurs fois). Ce qui signifie doubler le temps de copie des membres de la structure.

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:

  1. la convention d'appel: qu'est-ce que le compilateur enregistre automatiquement sur la pile lors de l'appel de cette fonction (généralement c'est le contenu de quelques registres). Si les membres de votre structure peuvent être copiés sur la pile en profitant de ce mécanisme, il n'y a pas de pénalité.
  2. le type de données du membre de la structure: si les registres de votre machine sont de 16 bits et que le type de données des membres de votre structure est de 64 bits, il ne rentre évidemment pas dans un seul registre, donc plusieurs opérations devront être effectuées juste pour une copie.
  3. le nombre de registres de votre machine: en supposant que vous ayez une structure avec un seul membre, un char (8 bits). Cela devrait entraîner la même surcharge lors du passage du paramètre par valeur ou par référence (en théorie). Mais il existe potentiellement un autre danger. Si votre architecture a des registres de données et d'adresses séparés, le paramètre passé par valeur occupera un registre de données et le paramètre passé par référence occupera un registre d'adresse. Le passage du paramètre par valeur exerce une pression sur les registres de données qui sont généralement plus utilisés que les registres d'adresses. Et cela peut provoquer des déversements sur la pile.

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

5
Pandrei

Remarque: les raisons de le faire dans un sens ou dans l'autre se chevauchent.

Quand passer/retourner par valeur:

  1. L'objet est un type fondamental comme int, double, pointeur.
  2. Une copie binaire de l'objet doit être faite - et l'objet n'est pas grand.
  3. La vitesse est importante et le passage par la valeur est plus rapide.
  4. 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

  1. Je ne sais pas si la valeur ou un pointeur vers la valeur est meilleur - c'est donc le choix par défaut.
  2. L'objet est grand.
  3. La vitesse est importante et le passage d'un pointeur sur l'objet est plus rapide.
  4. L'utilisation de la pile est critique. (Strictement, cela peut favoriser en valeur dans certains cas)
  5. Des modifications de l'objet transmis sont nécessaires.
  6. 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.

3
chux

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.

1
Peter A. Schneider