web-dev-qa-db-fra.com

Ce qui est plus rapide: allocation de pile ou allocation de tas

Cette question peut sembler assez élémentaire, mais c'est un débat que j'ai eu avec un autre développeur avec lequel je travaille.

Je prenais soin d’empiler les choses dans la mesure du possible, au lieu de les allouer en tas. Il me parlait et surveillait par-dessus mon épaule et a fait remarquer que ce n'était pas nécessaire, car ils ont les mêmes performances.

J'ai toujours eu l'impression que le temps de croissance de la pile était constant et que la performance de l'allocation de tas dépendait de la complexité actuelle du tas pour l'allocation (trouver un trou de la taille appropriée) et la désallocation (effondrement des trous pour réduire la fragmentation, comme de nombreuses implémentations de librairies standard prennent du temps à faire cela lors des suppressions si je ne me trompe pas).

Cela me semble être quelque chose qui serait probablement très dépendant du compilateur. Pour ce projet en particulier, j'utilise un compilateur Metrowerks pour l'architecture PPC . Un aperçu de cette combinaison serait très utile, mais en général, pour GCC et MSVC++, quel est le cas? L'allocation de tas est-elle moins performante que l'allocation de pile? N'y a-t-il pas de différence? Ou bien les différences sont-elles si petites que cela devient une micro-optimisation inutile.

484
Adam

L'allocation de pile est beaucoup plus rapide car elle ne fait que déplacer le pointeur de pile. En utilisant des pools de mémoire, vous pouvez obtenir des performances comparables avec l'allocation de tas, mais cela vient avec une légère complexité supplémentaire et ses propres problèmes.

De plus, pile contre tas n'est pas seulement une considération de performance; il vous en dit également beaucoup sur la durée de vie attendue des objets.

478

La pile est beaucoup plus rapide. Il n'utilise littéralement qu'une seule instruction sur la plupart des architectures, dans la plupart des cas, par exemple. sur x86:

sub esp, 0x10

(Cela déplace le pointeur de pile vers le bas de 0x10 octets et "alloue" ainsi ces octets à une variable.)

Bien sûr, la taille de la pile est très très finie, car vous saurez vite si vous abusez de l'allocation de pile ou essayez de faire de la récursion :-)

En outre, il n'y a pas de raison d'optimiser les performances du code qui n'en a pas besoin, tel que démontré par le profilage. L '"optimisation prématurée" pose souvent plus de problèmes que cela n'en vaut la peine.

Ma règle générale: si je sais que je vais avoir besoin de données au moment de la compilation, et que sa taille est inférieure à quelques centaines d'octets, je les empile en pile. Sinon je le tas-allouer.

163
Dan Lenski

Honnêtement, écrire un programme pour comparer les performances est une tâche banale:

#include <ctime>
#include <iostream>

namespace {
    class empty { }; // even empty classes take up 1 byte of space, minimum
}

int main()
{
    std::clock_t start = std::clock();
    for (int i = 0; i < 100000; ++i)
        empty e;
    std::clock_t duration = std::clock() - start;
    std::cout << "stack allocation took " << duration << " clock ticks\n";
    start = std::clock();
    for (int i = 0; i < 100000; ++i) {
        empty* e = new empty;
        delete e;
    };
    duration = std::clock() - start;
    std::cout << "heap allocation took " << duration << " clock ticks\n";
}

On dit que ne consistance idiote est le hobgoblin des petits esprits . Apparemment, les compilateurs optimisants sont les maîtres mots de beaucoup de programmeurs. Auparavant, cette discussion figurait au bas de la réponse, mais apparemment, les gens ne se laissent pas déranger de lire aussi loin, alors je la déplace ici pour éviter de poser des questions auxquelles j'ai déjà répondu.

Un compilateur optimiseur peut remarquer que ce code ne fait rien et qu'il peut tout optimiser. C'est le travail de l'optimiseur de faire ce genre de choses, et lutter contre l'optimiseur est une course dupe.

Je recommanderais de compiler ce code avec l'optimisation désactivée, car il n'existe aucun moyen efficace de duper tous les optimiseurs actuellement utilisés ou qui le seront ultérieurement.

Toute personne qui allume l'optimiseur puis se plaint de le combattre devrait être ridiculisée par le public.

Si je tenais à la précision à la nanoseconde, je n’utiliserais pas std::clock(). Si je souhaitais publier les résultats sous forme de thèse de doctorat, je ferais un plus gros travail à ce sujet et je comparerais probablement les logiciels GCC, Tendra/Ten15, LLVM, Watcom, Borland, Visual C++, Digital Mars, ICC et autres. Dans l'état actuel des choses, l'allocation de tas prend des centaines de fois plus de temps que l'allocation de pile, et je ne vois rien d'utile pour approfondir la question.

L'optimiseur a pour mission de se débarrasser du code que je teste. Je ne vois aucune raison de dire à l'optimiseur de s'exécuter, puis d'essayer de le tromper pour qu'il n'optimise pas réellement. Mais si je jugeais utile de le faire, je ferais l'une ou plusieurs des choses suivantes:

  1. Ajoutez un membre de données à empty et accédez à ce membre de données dans la boucle; mais si je ne lis que le membre de données, l'optimiseur peut effectuer un repliement constant et supprimer la boucle; si je n'écris jamais que dans le membre de données, l'optimiseur peut ignorer tout sauf la dernière itération de la boucle. De plus, la question n'était pas "Allocation de pile et accès aux données vs. Allocation de tas et accès aux données".

  2. Déclarez evolatile, mais volatile est souvent mal compilé (PDF).

  3. Prenez l'adresse de e à l'intérieur de la boucle (et attribuez-la éventuellement à une variable déclarée extern et définie dans un autre fichier). Mais même dans ce cas, le compilateur peut remarquer que - sur la pile au moins - e sera toujours alloué à la même adresse mémoire, puis effectuera un repliement constant comme dans (1) ci-dessus. Je reçois toutes les itérations de la boucle, mais l'objet n'est jamais réellement alloué.

Au-delà de l'évidence, ce critère est erroné en ce qu'il mesure à la fois l'allocation et la délocalisation, et la question initiale ne portait pas sur la délocalisation. Bien sûr, les variables allouées sur la pile sont automatiquement désallouées à la fin de leur portée. Par conséquent, ne pas appeler delete (1) fausserait les chiffres (la désaffectation de pile est incluse dans les chiffres concernant l'allocation de pile, il est donc juste de mesurer heap deallocation) et (2) provoquent une fuite de mémoire assez grave, sauf si nous conservons une référence au nouveau pointeur et appelons delete après avoir mesuré le temps.

Sur ma machine, avec g ++ 3.4.4 sous Windows, j'obtiens "0 ticks d'horloge" pour les allocations de pile et de tas pour toute allocation inférieure à 100 000 allocations, et même dans ce cas, "0 ticks d'horloge" pour l'allocation de pile et "15 ticks d'horloge "pour l’allocation de tas. Lorsque je mesure 10 000 000 d'allocations, l'allocation de pile prend 31 ticks d'horloge et l'allocation de tas prend 1562 ticks d'horloge.


Oui, un compilateur optimiseur peut éviter de créer les objets vides. Si je comprends bien, il se peut même que toute la première boucle soit perdue. Lorsque je suis passé à 10 000 000 itérations, l'allocation de pile a pris 31 ticks d'horloge et l'allocation de tas a nécessité 1562 ticks d'horloge. Je pense qu'il est prudent de dire que sans dire à g ++ d'optimiser l'exécutable, g ++ n'a pas élidé les constructeurs.


Dans les années écoulées depuis que j'ai écrit ceci, la préférence pour Stack Overflow a été de publier les performances à partir de versions optimisées. En général, je pense que c'est correct. Cependant, je pense toujours qu'il est idiot de demander au compilateur d'optimiser le code alors qu'en réalité, vous ne souhaitez pas l'optimiser. Cela me semble très similaire à payer un supplément pour le service de voiturier, mais en refusant de remettre les clés. Dans ce cas particulier, je ne veux pas que l'optimiseur s'exécute.

En utilisant une version légèrement modifiée du test (pour indiquer le point valide que le programme original n’a pas alloué quelque chose sur la pile à chaque boucle), et en compilant sans optimisations mais en se liant à des bibliothèques de versions (pour traiter le point valide 't veulent inclure tout ralentissement causé par la liaison aux bibliothèques de débogage):

#include <cstdio>
#include <chrono>

namespace {
    void on_stack()
    {
        int i;
    }

    void on_heap()
    {
        int* i = new int;
        delete i;
    }
}

int main()
{
    auto begin = std::chrono::system_clock::now();
    for (int i = 0; i < 1000000000; ++i)
        on_stack();
    auto end = std::chrono::system_clock::now();

    std::printf("on_stack took %f seconds\n", std::chrono::duration<double>(end - begin).count());

    begin = std::chrono::system_clock::now();
    for (int i = 0; i < 1000000000; ++i)
        on_heap();
    end = std::chrono::system_clock::now();

    std::printf("on_heap took %f seconds\n", std::chrono::duration<double>(end - begin).count());
    return 0;
}

affiche:

on_stack took 2.070003 seconds
on_heap took 57.980081 seconds

sur mon système une fois compilé avec la ligne de commande cl foo.cc /Od /MT /EHsc.

Vous n'êtes peut-être pas d'accord avec mon approche pour obtenir une version non optimisée. C'est bien: n'hésitez pas à modifier le point de repère autant que vous le souhaitez. Lorsque j'active l'optimisation, je reçois:

on_stack took 0.000000 seconds
on_heap took 51.608723 seconds

Non pas parce que l'allocation de pile est en fait instantanée, mais parce que tout compilateur à moitié décent peut remarquer que on_stack ne fait rien d'utile et peut être optimisé. GCC sur mon ordinateur portable Linux remarque également que on_heap ne fait rien d’utile, et l’optimise également:

on_stack took 0.000003 seconds
on_heap took 0.000002 seconds
115
Max Lybbert

Une chose intéressante que j’ai apprise sur l’allocation de pile/tas sur le processeur Xbox 360 Xenon, qui peut également s’appliquer à d’autres systèmes multicœurs, est que l’allocation sur le tas entraîne la saisie d’une section critique afin d’arrêter tous les autres cœurs pas de conflit. Ainsi, dans une boucle serrée, Stack Allocation était la solution idéale pour les tableaux de taille fixe, car elle empêchait les décrochages.

Cela peut constituer un autre facteur d'accélération à prendre en compte si vous codez pour des processus multicœurs/multiproc, en ce sens que votre allocation de pile ne sera visible que par le noyau exécutant votre fonction étendue et que cela n'affectera pas les autres cœurs/processeurs.

29
Furious Coder

Vous pouvez écrire un allocateur de tas spécial pour des tailles d’objets très performantes. Cependant, l'allocateur de tas général n'est pas particulièrement performant.

Je suis également d'accord avec Torbjörn Gyllebring sur la durée de vie attendue des objets. Bon point!

18

Je ne pense pas que l'allocation de pile et l'allocation de tas soient généralement interchangeables. J'espère aussi que les performances des deux sont suffisantes pour un usage général.

Je le recommande fortement pour les petits articles, celui qui convient le mieux à l'étendue de l'allocation. Pour les gros articles, le tas est probablement nécessaire.

Sur les systèmes d'exploitation 32 bits comportant plusieurs threads, la pile est souvent assez limitée (bien que typiquement d'au moins quelques mb), car l'espace d'adressage doit être découpé et qu'une pile de threads se rencontrera tôt ou tard. Sur les systèmes à un seul thread (glibc de Linux tout de même), la limitation est beaucoup moins importante car la pile ne peut que grandir et grandir.

Sur les systèmes d'exploitation 64 bits, l'espace d'adressage est suffisant pour créer des piles de threads assez volumineuses.

7
MarkR

En général, l’allocation de pile consiste simplement à soustraire du registre du pointeur de pile. C'est beaucoup plus rapide que de chercher un tas.

Parfois, l’allocation de pile nécessite l’ajout d’une ou plusieurs pages de mémoire virtuelle. L'ajout d'une nouvelle page de mémoire mise à zéro ne nécessite pas la lecture d'une page à partir d'un disque. Par conséquent, la recherche est toujours beaucoup plus rapide que la recherche d'un segment de mémoire (en particulier si une partie du segment a également été paginée). Dans de rares cas, et vous pourriez construire un tel exemple, il se trouve que suffisamment d'espace est disponible dans une partie du tas qui se trouve déjà dans la RAM, mais l'allocation d'une nouvelle page à la pile doit attendre qu'une autre page soit écrite. sur le disque. Dans cette situation rare, le tas est plus rapide.

6

Mis à part l'avantage des performances d'ordre de grandeur par rapport à l'allocation de tas, l'allocation de pile est préférable pour les applications serveur à exécution longue. Même les tas les mieux gérés finissent par être tellement fragmentés que les performances des applications se dégradent.

6
Jay

Une pile a une capacité limitée, contrairement à un tas. La pile typique d'un processus ou d'un thread est d'environ 8 Ko. Vous ne pouvez pas changer la taille une fois qu'elle est allouée.

Une variable de pile suit les règles de portée, contrairement à une pile. Si votre pointeur d'instruction dépasse une fonction, toutes les nouvelles variables associées à la fonction disparaissent.

Le plus important de tous, vous ne pouvez pas prédire à l'avance la chaîne globale des appels de fonction. Donc, une simple allocation de 200 octets de votre part peut générer un débordement de pile. Ceci est particulièrement important si vous écrivez une bibliothèque, pas une application.

4
yogman

Ce n'est pas simplement l'allocation de pile qui est plus rapide. Vous gagnez également beaucoup en utilisant des variables de pile. Ils ont une meilleure localité de référence. Enfin, la désallocation est également beaucoup moins chère.

3
MSalters

L'allocation de pile sera presque toujours aussi rapide ou plus rapide que l'allocation de tas, bien qu'il soit certainement possible pour un allocateur de tas d'utiliser simplement une technique d'allocation basée sur la pile.

Cependant, la performance globale de l'allocation par pile/tas (ou légèrement mieux, de l'allocation locale par rapport à externe) pose des problèmes plus importants. En règle générale, l'allocation de tas (externe) est lente car elle traite de nombreux types d'allocations et de modèles d'allocation. Réduire la portée de l'allocateur que vous utilisez (en le rendant local par rapport à l'algorithme/code) aura tendance à augmenter les performances sans changements majeurs. L'ajout d'une meilleure structure à vos modèles d'allocation, par exemple, forcer un ordre LIFO sur les paires d'allocation et de désallocation peut également améliorer les performances de votre allocateur en utilisant l'allocateur d'une manière plus simple et plus structurée. Vous pouvez aussi utiliser ou écrire un allocateur adapté à votre modèle d’allocation particulier; la plupart des programmes allouent fréquemment quelques tailles discrètes, de sorte qu'un segment de mémoire basé sur un tampon lookaside de quelques tailles fixes (de préférence connues) fonctionnera extrêmement bien. Windows utilise son tas faible fragmentation pour cette raison même.

D'autre part, l'allocation basée sur la pile sur une plage de mémoire 32 bits est également périlleuse si vous avez trop de threads. Les piles ont besoin d'une plage de mémoire contiguë. Par conséquent, plus vous avez de threads, plus vous aurez besoin d'espace d'adressage virtuel pour pouvoir s'exécuter sans débordement de pile. Ce ne sera pas un problème (pour le moment) avec 64 bits, mais cela peut certainement causer des ravages dans les programmes de longue durée avec beaucoup de threads. Il est toujours difficile de gérer le manque d'espace d'adressage virtuel en raison de la fragmentation.

3
MSN

Le plus gros problème de l’allocation de tas par rapport à l’allocation de pile est probablement que l’allocation de tas est en général une opération sans limite, et que vous ne pouvez donc pas l’utiliser lorsque la synchronisation est un problème.

Pour d'autres applications où la synchronisation n'est pas un problème, cela n'a peut-être pas autant d'importance, mais si vous allouez beaucoup, cela affectera la vitesse d'exécution. Essayez toujours d'utiliser la pile pour de la mémoire de courte durée et souvent allouée (par exemple, des boucles) et aussi longtemps que possible - affectez le tas au démarrage de l'application.

3
larsivi

Je pense que la durée de vie est cruciale et que la chose allouée doit être construite de manière complexe. Par exemple, dans la modélisation pilotée par transaction, vous devez généralement renseigner et transmettre une structure de transaction avec un tas de champs aux fonctions d'opération. Regardez la norme OSCI SystemC TLM-2.0 pour un exemple.

L'attribution de ces éléments sur la pile près de l'appel à l'opération a tendance à générer d'énormes frais généraux, car la construction est coûteuse. Le bon moyen consiste à allouer sur le tas et à réutiliser les objets de transaction par regroupement ou par une stratégie simple telle que "ce module n'a besoin que d'un seul objet de transaction".

C'est beaucoup plus rapide que d'allouer l'objet à chaque appel d'opération.

La raison en est simplement que l'objet a une construction coûteuse et une durée de vie utile assez longue.

Je dirais: essayez les deux et voyez ce qui fonctionne le mieux dans votre cas, car cela dépend vraiment du comportement de votre code.

3
jakobengblom2

Préoccupations spécifiques au langage C++

Tout d’abord, il n’existe pas d’allocation dite "pile" ou "tas" imposée par C++ . Si vous parlez d'objets automatiques dans les portées 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é"; ce dernier est "dynamique" dans le jargon C++.) La mémoire allouée dynamiquement est sur le free store, pas nécessairement sur "le tas", bien que ce dernier soit souvent le (par défaut) implementation.

Bien que, conformément aux machine abstraite règles sémantiques, les objets automatiques occupent toujours de la mémoire, une implémentation C++ conforme est autorisée à ignorer ce fait quand il peut prouver que cela n’a pas d’importance (quand cela ne modifie pas le comportement observable de le programme). Cette permission est accordée par règle as-if dans ISO C++, qui est également la clause générale permettant les optimisations usuelles (et il existe également une règle presque identique dans ISO C). Outre la règle as-if, ISO C++ dispose également de règles de copie pour permettre l'omission de créations spécifiques d'objets. Les appels de constructeur et de destructeur impliqués sont ainsi omis. En conséquence, les objets automatiques (le cas échéant) de ces constructeurs et destructeurs sont également éliminés, par rapport à la sémantique abstraite naïve impliquée par le code source.

D'autre part, l'allocation de magasin gratuit est définitivement "allocation" par conception. Selon les règles ISO C++, une telle allocation peut être réalisée par un appel d'une fonction d'allocation. Cependant, depuis ISO C++ 14, il existe ne nouvelle règle (non-as-if) pour permettre la fusion d'appels de fonction d'allocation globale (c'est-à-dire _::operator new_) dans des cas spécifiques. Ainsi, certaines parties des opérations d'allocation dynamique peuvent également être non-opérables, comme dans le cas d'objets automatiques.

Les fonctions d'allocation allouent des ressources de mémoire. Les objets peuvent également être alloués en fonction de l'allocation à l'aide d'allocateurs. Pour les objets automatiques, ils sont directement présentés - bien que la mémoire sous-jacente puisse être consultée 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 libre, car aucun moyen de déplacer les ressources ailleurs.

Toutes les autres préoccupations sortent du cadre 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 ni certaines sortes de suites de première classe (par exemple, par le célèbre call/cc ), il n'existe aucun moyen de manipuler directement les cadres d'enregistrement d'activation - lorsque la mise en œuvre est nécessaire pour placer les objets automatiques à. En l'absence d'interopérabilité (non portable) 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 triviale. Par exemple, lorsque la fonction appelée est en ligne, les images peuvent être efficacement fusionnées, il n’ya donc aucun moyen de montrer quelle est la "répartition".

Cependant, une fois que les interops sont respectés, la situation devient complexe. Une implémentation typique de C++ exposera la capacité d'interopérabilité sur ISA (architecture d'ensemble d'instructions) avec quelques conventions d'appel comme limite binaire partagée avec le système natif (ordinateur de niveau ISA). code. Cela serait explicitement coûteux, notamment pour le maintien du pointeur de pile, qui est souvent directement détenu par un registre de niveau ISA (avec probablement des instructions spécifiques à accéder à la machine). Le pointeur de pile indique la limite du cadre supérieur de l'appel de fonction (actuellement actif). Lorsqu'un appel de fonction est entré, une nouvelle image est nécessaire et le pointeur de pile est ajouté ou soustrait (selon la convention d'ISA) d'une valeur non inférieure à la taille d'image requise. La trame est alors dite allouée lorsque le pointeur de pile après les opérations. Les paramètres des fonctions peuvent également être transmis au cadre de la pile, en fonction de la convention d'appel utilisée pour l'appel. Le cadre peut contenir la mémoire d'objets automatiques (y compris probablement les paramètres) spécifiés par le code source C++. Au sens de telles implémentations, ces objets sont "alloués". Lorsque la commande 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 dans l'état antérieur à l'appel (enregistré précédemment conformément à la convention d'appel). Cela peut être considéré comme une "désallocation". Ces opérations transforment effectivement l'activation en une structure de données LIFO. C'est pourquoi on l'appelle souvent " la pile (d'appel) ". Le pointeur de pile indique effectivement la position la plus haute de la pile.

Étant donné que la plupart des implémentations C++ (en particulier celles qui ciblent du code natif au niveau ISA et qui utilisent le langage Assembly comme résultat immédiat) utilisent des stratégies similaires, le schéma "d'allocation", qui prête à confusion, est populaire. De telles allocations (ainsi que des désallocations) utilisent des cycles machine et cela peut coûter cher lorsque les appels (non optimisés) sont fréquents, même si les microarchitectures de processeurs modernes peuvent avoir des optimisations complexes implémentées par le matériel pour le modèle de code commun moteur de la pile lors de la mise en œuvre des instructions Push/POP).

Mais de toute façon, en général , il est vrai que le coût de l’allocation des trames de la pile est nettement inférieur à celui d’un appel à une fonction d’allocation exploitant le magasin libre (à moins qu’elle ne soit totalement optimisée) , qui peut avoir lui-même des centaines (si ce n'est des millions d'opérations) pour maintenir le pointeur de pile et d'autres états. Les fonctions d’allocation sont généralement basées sur une API fournie par l’environnement hébergé (par exemple, une exécution fournie par le système d’exploitation). À la différence du but de conserver des objets automatiques pour les appels de fonctions, de telles attributions sont générales, elles n'auront donc pas de structure de trame comme une pile. Traditionnellement, ils allouent de l'espace à partir du stockage en pool appelé tas (ou plusieurs tas). Contrairement à la "pile", le concept "tas" n'indique pas ici la structure de données utilisée; dérivé des premières implémentations linguistiques . (En passant, la pile d’appels se voit généralement attribuer 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 d’un tas beaucoup plus compliquées (que Push ou pop of pop). cadres de pile), et il n’est guère possible d’être optimisé 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, de sorte qu'il a une assez bonne localité. C'est sympa à cacher. OTOH, la mémoire allouée de manière aléatoire dans le magasin gratuit n'a pas cette propriété. Depuis ISO C++ 17, il existe des modèles de ressources de pool fournis par _<memory>_. Le but direct de cette interface est de permettre aux résultats d'allocations consécutives d'être rapprochés en mémoire. Cela reconnaît le fait que cette stratégie est généralement bonne pour la performance avec les implémentations contemporaines, par exemple. être amical à cacher dans les architectures modernes. Ceci concerne les performances de accès plutôt que allocation, cependant.

Simultanéité

L'attente d'un accès simultané à la mémoire peut avoir différents effets 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 parmi les threads d'un processus. Pour de tels tas, les fonctions d'allocation et de désallocation doivent protéger la structure de données administratives internes partagées de la course des données. En conséquence, les allocations de segment de mémoire et les désallocations peuvent entraîner une surcharge supplémentaire en raison d'opérations de synchronisation internes.

Efficacité spatiale

En raison de la nature des cas d'utilisation et des structures de données internes, les tas peuvent être affectés par --- fragmentation de la mémoire , contrairement à la pile. Cela n'a pas d'impact direct sur les performances d'allocation de mémoire, mais dans un système avec mémoire virtuelle , une faible utilisation de l'espace peut dégénérer les performances globales de l'accès à la mémoire. Cela est particulièrement affreux lorsque le disque dur est utilisé comme échange de mémoire physique. Cela peut entraîner une latence assez longue, parfois des milliards de cycles.

Limites des allocations de pile

Bien que les allocations de pile aient souvent des performances supérieures à celles de tas, 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++. Des implémentations telles que alloca et le VLA (tableau de longueur variable) de G ++ offrent des extensions, mais il existe des raisons de les éviter. (Le code source IIRC, Linux supprime l'utilisation de VLA récemment.) (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 la pile. Ceci est souvent appelé débordement de pile (hmm, l'étymologie de ce site), mais probablement plus précisément, stack overun. En réalité, cela provoque souvent un accès invalide à la mémoire 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 le rend indéfini lorsque la ressource est épuisée . Méfiez-vous des espaces disponibles pour les objets automatiques.

Si l'espace de pile est épuisé, trop d'objets sont alloués dans la pile, ce qui peut être dû à un trop grand nombre d'appels de fonctions actifs ou à une utilisation incorrecte des objets automatiques. De tels cas peuvent suggérer l'existence de bugs, par ex. 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 langues nécessitant la prise en charge des appels actifs non liés (où la profondeur de l'appel est limitée à la mémoire totale), il est impossible d'utiliser la pile d'appels natifs (contemporains) directement comme enregistrement d'activation de la langue cible, comme d'habitude Implémentations C++. Pour contourner le problème, il est nécessaire de trouver d'autres moyens de construire des enregistrements d'activation. Par exemple, SML/NJ alloue explicitement des cadres sur le tas et utilise piles de cactus . L'attribution compliquée de telles trames d'enregistrement d'activation n'est généralement pas rapide comme les trames de la pile d'appels. Cependant, si ces langages sont implémentés plus loin avec la garantie de récursion correcte , 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érence, mais natif). les valeurs primitives qui peuvent être mappées un par un à des objets C++ non partagés) est encore plus compliqué avec plus de pénalité de performances en général. Lorsque vous utilisez C++ pour implémenter de tels langages, il est difficile d'estimer les impacts sur les performances.

3
FrankHB

L'allocation de pile est un couple d'instructions alors que l'allocateur de tas le plus rapide que je connaisse (TLSF) utilise en moyenne environ 150 instructions. De plus, les allocations de pile ne nécessitent pas de verrou car elles utilisent le stockage local du thread, ce qui représente un autre gain considérable en termes de performances. Les allocations de pile peuvent donc être plus rapides de 2 à 3 ordres de grandeur en fonction de la charge de votre environnement multithread.

En général, l’allocation de tas est votre dernier recours si vous vous souciez de la performance. Une option intermédiaire viable peut être un allocateur de pool fixe qui ne représente également que quelques instructions et a très peu de charge supplémentaire par allocation; il est donc idéal pour les petits objets de taille fixe. En revanche, il ne fonctionne qu'avec des objets de taille fixe, n'est pas intrinsèquement thread-safe et a des problèmes de fragmentation de bloc.

3
Andrei Pokrovsky

Il y a une remarque générale à faire sur de telles optimisations.

L'optimisation obtenue est proportionnelle à la durée pendant laquelle le compteur de programme est réellement dans ce code.

Si vous échantillonnez le compteur de programme, vous découvrirez où il passe son temps, et cela se trouve généralement dans une infime partie du code, et souvent dans les routines de bibliothèque que vous n'avez aucun contrôle.

Ce n'est que si vous trouvez qu'il passe beaucoup de temps dans l'allocation de tas de vos objets qu'il sera nettement plus rapide de les empiler en pile.

2
Mike Dunlavey

Comme d'autres l'ont dit, l'allocation de pile est généralement beaucoup plus rapide.

Toutefois, si la copie de vos objets est coûteuse, l’allocation sur la pile peut entraîner de lourdes pertes de performances lorsque vous utilisez les objets si vous ne faites pas attention.

Par exemple, si vous allouez quelque chose sur la pile, puis le placez dans un conteneur, il aurait été préférable d'allouer le tas et de stocker le pointeur dans le conteneur (par exemple, avec un std :: shared_ptr <>). La même chose est vraie si vous transmettez ou retournez des objets par valeur et d’autres scénarios similaires.

Le fait est que bien que l'allocation de pile soit généralement meilleure que l'allocation de tas dans de nombreux cas, parfois, si vous vous en sortez mal quand l'allocation ne correspond pas au modèle de calcul, elle peut causer plus de problèmes qu'elle n'en résout.

2
wjl
class Foo {
public:
    Foo(int a) {

    }
}
int func() {
    int a1, a2;
    std::cin >> a1;
    std::cin >> a2;

    Foo f1(a1);
    __asm Push a1;
    __asm lea ecx, [this];
    __asm call Foo::Foo(int);

    Foo* f2 = new Foo(a2);
    __asm Push sizeof(Foo);
    __asm call operator new;//there's a lot instruction here(depends on system)
    __asm Push a2;
    __asm call Foo::Foo(int);

    delete f2;
}

Ce serait comme ça en asm. Lorsque vous êtes dans func, le f1 et le pointeur f2 ont été alloués sur la pile (stockage automatisé). Et au fait, Foo f1(a1) n'a pas d'effet d'instruction sur le pointeur de pile (esp), il a été alloué, si func veut obtenir le membre f1, son instruction est quelque chose comme ceci: lea ecx [ebp+f1], call Foo::SomeFunc(). Une autre chose que la pile alloue peut faire penser à la mémoire que quelque chose comme FIFO, le FIFO vient de se produire lorsque vous entrez dans une fonction, si vous êtes dans la fonction et allouez quelque chose comme int i = 0 , il n'y a pas eu de Push.

2
bitnick

Nous avons déjà mentionné que l'allocation de pile ne fait que déplacer le pointeur de pile, c'est-à-dire une instruction unique sur la plupart des architectures. Comparez cela à ce que généralement se produit dans le cas d'une allocation de tas.

Le système d'exploitation conserve des parties de la mémoire libre sous forme de liste chaînée avec les données utiles comprenant le pointeur sur l'adresse de départ de la partie libre et la taille de la partie libre. Pour allouer X octets de mémoire, la liste des liens est parcourue et chaque note est visitée dans l'ordre, en vérifiant si sa taille est au moins égale à X. Lorsqu'une partie de taille P> = X est trouvée, P est scindé en deux parties avec tailles X et PX. La liste liée est mise à jour et le pointeur sur la première partie est renvoyé.

Comme vous pouvez le constater, l’allocation de tas dépend de nombreux facteurs, tels que la quantité de mémoire demandée, le degré de fragmentation de la mémoire, etc.

1
Nikhil

En général, l'allocation de pile est plus rapide que l'allocation de tas, comme mentionné par presque toutes les réponses ci-dessus. Une pile Push ou pop vaut O (1), tandis que l'allocation ou la libération d'un segment de mémoire peut nécessiter une analyse des allocations précédentes. Cependant, vous ne devriez généralement pas allouer des boucles trop exigeantes en performances, le choix dépendra donc généralement d'autres facteurs.

Il peut être intéressant de faire cette distinction: vous pouvez utiliser un "allocateur de pile" sur le tas. Strictement parlant, je considère que l'allocation de pile désigne la méthode d'allocation réelle plutôt que l'emplacement de l'allocation. Si vous allouez beaucoup de choses dans la pile de programmes, cela pourrait être mauvais pour diverses raisons. D'un autre côté, le meilleur choix que vous puissiez faire pour une méthode d'allocation consiste à utiliser une méthode de pile pour allouer sur le tas lorsque cela est possible.

Puisque vous avez parlé de Metrowerks et de PPC, je suppose que vous voulez dire la Wii. Dans ce cas, la mémoire est rare et l'utilisation d'une méthode d'allocation de pile, dans la mesure du possible, garantit que vous ne gaspillez pas la mémoire en fragments. Bien sûr, cela nécessite beaucoup plus de soin que les méthodes d'allocation de tas "normales". Il est sage d'évaluer les compromis pour chaque situation.

1
Dan Olson

Remarquez que les considérations ne concernent généralement pas la vitesse et les performances lors du choix de l'allocation pile/pile. La pile agit comme une pile, ce qui signifie qu'elle est bien adaptée pour pousser des blocs et les faire sauter à nouveau, dernier entré, premier sorti. L'exécution des procédures ressemble également à une pile. La dernière procédure entrée est la première à être quittée. Dans la plupart des langages de programmation, toutes les variables nécessaires à une procédure ne sont visibles que lors de son exécution. Elles sont donc poussées lors de la saisie d'une procédure et sorties de la pile à la sortie ou au retour.

Maintenant, pour un exemple où la pile ne peut pas être utilisée:

Proc P
{
  pointer x;
  Proc S
  {
    pointer y;
    y = allocate_some_data();
    x = y;
  }
}

Si vous allouez de la mémoire dans la procédure S et la placez sur la pile, puis quittez S, les données allouées seront extraites de la pile. Mais la variable x dans P pointait également vers ces données, donc x pointe maintenant à un endroit situé sous le pointeur de pile (supposons que la pile se développe vers le bas) avec un contenu inconnu. Le contenu peut toujours être présent si le pointeur de pile est simplement déplacé vers le haut sans effacer les données situées en dessous, mais si vous commencez à allouer de nouvelles données sur la pile, le pointeur x pourrait en fait pointer vers ces nouvelles données.

1

Ne faites jamais d'hypothèses prématurées car d'autres codes d'application et utilisations peuvent avoir une incidence sur votre fonction. Donc, examiner la fonction, c'est que l'isolement ne sert à rien.

Si vous êtes sérieux avec l'application, alors appliquez VTune ou utilisez un outil de profilage similaire et examinez les points chauds.

Ketan

0
Ketan