Problèmes spécifiques au langage C ++
Tout d'abord, il n'y a pas d'allocation dite "pile" ou "tas" mandatée par C ++ . Si vous parlez d'objets automatiques dans des étendues de bloc, ils ne sont même pas "alloués". (BTW, la durée de stockage automatique en C n'est certainement PAS la même chose que "allouée"; cette dernière est "dynamique" dans le langage C ++.) La mémoire allouée dynamiquement se trouve sur le magasin gratuit , pas nécessairement sur "le tas", bien que le cette dernière est souvent l' implémentation (par défaut) .
Bien que selon les règles sémantiques de la machine abstraite , les objets automatiques occupent toujours la mémoire, une implémentation C ++ conforme est autorisée à ignorer ce fait lorsqu'elle peut prouver que cela n'a pas d'importance (lorsqu'elle ne modifie pas le comportement observable du programme). Cette autorisation est accordée par la règle as-if dans ISO C ++, qui est également la clause générale permettant les optimisations habituelles (et il existe également presque la même règle dans ISO C). Outre la règle de simulation, ISO C ++ doit également règles d'élision de copiepermettre l'omission de créations spécifiques d'objets. Les appels constructeur et destructeur impliqués sont ainsi omis. En conséquence, les objets automatiques (le cas échéant) dans ces constructeurs et destructeurs sont également éliminés, par rapport à la sémantique abstraite naïve impliquée par le code source.
D'un autre côté, l'allocation gratuite de magasins est définitivement «allocation» par conception. Selon les règles ISO C ++, une telle allocation peut être obtenue par un appel d'une fonction d'allocation . Cependant, depuis ISO C ++ 14, il existe une nouvelle règle (non-as-if) pour permettre la fusion des ::operator new
appels de fonction d'allocation globale (c'est-à-dire ) dans des cas spécifiques. Ainsi, certaines parties des opérations d'allocation dynamique peuvent également être sans opération comme dans le cas des objets automatiques.
Les fonctions d'allocation allouent des ressources de mémoire. Les objets peuvent être davantage alloués en fonction de l'allocation à l'aide d'allocateurs. Pour les objets automatiques, ils sont présentés directement - bien que la mémoire sous-jacente soit accessible et utilisée pour fournir de la mémoire à d'autres objets (par placement new
), mais cela n'a pas beaucoup de sens en tant que magasin gratuit, car il n'y a aucun moyen de déplacer le ailleurs.
Toutes les autres préoccupations sont hors de portée de C ++. Néanmoins, ils peuvent être encore importants.
A propos des implémentations de C ++
C ++ n'expose pas les enregistrements d'activation réifiés ou certaines sortes de continuations de première classe (par exemple par le célèbre call/cc
), il n'y a aucun moyen de manipuler directement les trames d'enregistrement d'activation - où l'implémentation doit placer les objets automatiques. Une fois qu'il n'y a pas d'interopérations (non portables) avec l'implémentation sous-jacente (code non portable "natif", tel que le code d'assemblage en ligne), une omission de l'allocation sous-jacente des trames peut être assez banale. Par exemple, lorsque la fonction appelée est en ligne, les trames peuvent être efficacement fusionnées dans d'autres, il n'y a donc aucun moyen de montrer ce qu'est "l'allocation".
Cependant, une fois les interopérations respectées, les choses deviennent complexes. Une implémentation typique de C ++ exposera la capacité d'interopérabilité sur ISA (architecture de jeu d'instructions) avec certaines conventions d'appel comme frontière binaire partagée avec le code natif (machine de niveau ISA). Cela serait explicitement coûteux, notamment lors de la maintenance du pointeur de pile , qui est souvent directement détenu par un registre de niveau ISA (avec probablement des instructions machine spécifiques auxquelles accéder). Le pointeur de pile indique la limite de la trame supérieure de l'appel de fonction (actuellement actif). Lorsqu'un appel de fonction est entré, une nouvelle trame est nécessaire et le pointeur de pile est ajouté ou soustrait (selon la convention d'ISA) par une valeur non inférieure à la taille de trame requise. La trame est alors dite allouéelorsque le pointeur de pile après les opérations. Les paramètres des fonctions peuvent également être transmis à la trame de pile, selon la convention d'appel utilisée pour l'appel. Le cadre peut contenir la mémoire d'objets automatiques (incluant probablement les paramètres) spécifiés par le code source C ++. Dans le sens de telles implémentations, ces objets sont "alloués". Lorsque le contrôle quitte l'appel de fonction, la trame n'est plus nécessaire, elle est généralement libérée en restaurant le pointeur de pile à l'état avant l'appel (enregistré précédemment selon la convention d'appel). Cela peut être considéré comme une «désallocation». Ces opérations font de l'enregistrement d'activation une structure de données LIFO efficace, il est donc souvent appelé " la pile (d'appel) ".
Étant donné que la plupart des implémentations C ++ (en particulier celles ciblant le code natif de niveau ISA et utilisant le langage d'assemblage comme sortie immédiate) utilisent des stratégies similaires comme celle-ci, un tel schéma d'allocation déroutant est populaire. De telles allocations (ainsi que des désallocations) passent des cycles machine, et cela peut être coûteux lorsque les appels (non optimisés) se produisent fréquemment, même si les microarchitectures CPU modernes peuvent avoir des optimisations complexes implémentées par le matériel pour le modèle de code commun (comme l'utilisation d'un moteur de pile dans la mise en œuvre PUSH
/ POP
instructions).
Mais de toute façon, en général, il est vrai que le coût de l'allocation de trames de pile est nettement inférieur à un appel à une fonction d'allocation exploitant le magasin gratuit (à moins qu'il ne soit totalement optimisé) , qui lui-même peut avoir des centaines (sinon des millions de :-) opérations pour maintenir le pointeur de pile et d'autres états. Les fonctions d'allocation sont généralement basées sur l'API fournie par l'environnement hébergé (par exemple le runtime fourni par le système d'exploitation). Différentes du but de la conservation d'objets automatiques pour les appels de fonctions, ces allocations sont générales, donc elles n'auront pas de structure de trame comme une pile. Traditionnellement, ils allouent de l'espace à partir du stockage de pool appelé tas (ou plusieurs tas). Différent de la "pile", le concept de "tas" n'indique pas ici la structure de données utilisée;il est dérivé des premières implémentations de langage il y a des décennies . (BTW, la pile d'appels est généralement allouée avec une taille fixe ou spécifiée par l'utilisateur à partir du tas par l'environnement au démarrage du programme ou du thread.) La nature des cas d'utilisation rend les allocations et les désallocations à partir d'un tas beaucoup plus compliquées (que push ou pop de stack frames), et difficilement optimisables directement par le matériel.
Effets sur l'accès à la mémoire
L'allocation de pile habituelle place toujours le nouveau cadre en haut, donc il a une assez bonne localité. Ceci est convivial pour le cache. OTOH, la mémoire allouée au hasard dans le magasin gratuit n'a pas une telle propriété. Depuis ISO C ++ 17, il existe des modèles de ressources de pool fournis par <memory>
. Le but direct d'une telle interface est de permettre aux résultats d'allocations consécutives d'être rapprochées en mémoire. Cela reconnaît le fait que cette stratégie est généralement bonne pour les performances avec les implémentations contemporaines, par exemple en étant conviviale pour la mise en cache dans les architectures modernes. Il s'agit cependant de la performance de l' accès plutôt que de l' allocation .
Accès simultané
L'attente d'un accès simultané à la mémoire peut avoir des effets différents entre la pile et les tas. Une pile d'appels appartient généralement exclusivement à un thread d'exécution dans une implémentation C ++. OTOH, les tas sont souvent partagés entre les threads d'un processus. Pour de tels tas, les fonctions d'allocation et de désallocation doivent protéger la structure des données administratives internes partagées de la course aux données. Par conséquent, les allocations de tas et les désallocations peuvent entraîner des frais supplémentaires en raison des opérations de synchronisation interne.
Efficacité spatiale
En raison de la nature des cas d'utilisation et des structures de données internes, les tas peuvent souffrir de la fragmentation de la mémoire interne , contrairement à la pile. Cela n'a pas d'impact direct sur les performances de l'allocation de mémoire, mais dans un système avec mémoire virtuelle , une faible efficacité de l'espace peut dégénérer les performances globales de l'accès à la mémoire. C'est particulièrement affreux lorsque le disque dur est utilisé comme échange de mémoire physique. Il peut provoquer une latence assez longue - parfois des milliards de cycles.
Limitations des allocations de pile
Bien que les allocations de pile soient souvent plus performantes que les allocations de tas en réalité, cela ne signifie certainement pas que les allocations de pile peuvent toujours remplacer les allocations de tas.
Tout d'abord, il n'y a aucun moyen d'allouer de l'espace sur la pile avec une taille spécifiée au moment de l'exécution de manière portable avec ISO C ++. Il existe des extensions fournies par des implémentations telles alloca
que le VLA (tableau de longueur variable) de G ++, mais il y a des raisons de les éviter. (IIRC, la source Linux supprime récemment l'utilisation de VLA.) (Notez également que ISO C99 a mandaté VLA, mais ISO C11 rend le support facultatif.)
Deuxièmement, il n'existe aucun moyen fiable et portable de détecter l'épuisement de l'espace de pile. Ceci est souvent appelé débordement de pile (hmm, l'étymologie de ce site) , mais probablement plus précisément, dépassement de pile . En réalité, cela provoque souvent un accès à la mémoire invalide, et l'état du programme est alors corrompu (... ou pire, une faille de sécurité). En fait, ISO C ++ n'a pas de concept de «pile» et rend le comportement indéfini lorsque la ressource est épuisée . Soyez prudent quant à la place qui doit être laissée aux objets automatiques.
Si l'espace de la pile est épuisé, il y a trop d'objets alloués dans la pile, ce qui peut être dû à trop d'appels de fonctions actifs ou à une mauvaise utilisation des objets automatiques. De tels cas peuvent suggérer l'existence de bogues, par exemple un appel de fonction récursif sans conditions de sortie correctes.
Néanmoins, des appels récursifs profonds sont parfois souhaités. Dans les implémentations de langages nécessitant la prise en charge des appels actifs non liés (où la profondeur des appels n'est limitée que par la mémoire totale), il est impossible d'utiliser la pile d'appels natifs (contemporaine) directement comme enregistrement d'activation de la langue cible comme les implémentations C ++ typiques. Pour contourner le problème, d'autres méthodes de construction des enregistrements d'activation sont nécessaires. Par exemple, SML / NJ alloue explicitement des trames sur le tas et utilise des piles de cactus . L'allocation compliquée de telles trames d'enregistrement d'activation n'est généralement pas aussi rapide que les trames de pile d'appels. Cependant, si de tels langages sont implémentés davantage avec la garantie d' une récursivité de queue appropriée, l'allocation directe de pile dans le langage objet (c'est-à-dire que "l'objet" dans le langage n'est pas stocké en tant que références, mais les valeurs primitives natives qui peuvent être mappées un à un sur des objets C ++ non partagés) est encore plus compliquée avec plus pénalité de performance en général. Lorsque vous utilisez C ++ pour implémenter de tels langages, il est difficile d'estimer les impacts sur les performances.