web-dev-qa-db-fra.com

Comment déterminer l'utilisation maximale de la pile?

Quelles méthodes sont disponibles pour déterminer la taille de pile optimale pour un système embarqué/contraint en mémoire? S'il est trop grand, la mémoire est gaspillée et pourrait être utilisée ailleurs. Cependant, s'il est trop petit, nous obtenons l'homonyme de ce site Web ...

Pour essayer de faire démarrer les choses: Jack Ganssle déclare dans L'art de concevoir des systèmes embarqués que, "Avec l'expérience, on apprend la manière standard et scientifique de calculer la taille appropriée pour une pile: choisissez une taille au hasard et espérez. " Quelqu'un peut-il faire mieux que cela?

Un exemple plus spécifique a été demandé. Alors, que diriez-vous d'un programme C ciblant une MSP430 MC avec 2 Ko de RAM en utilisant la chaîne d'outils IAR Embedded Workbench sans système d'exploitation? Ce IDE peut afficher le contenu et l'utilisation de la pile lors de l'utilisation d'un débogueur JTAG.

43
Judge Maygarden

La façon la plus courante de déterminer l'utilisation la plus profonde de la pile est d'initialiser la mémoire de la pile avec une valeur connue mais inhabituelle, puis de voir périodiquement (ou à la fin d'un gros test) où s'arrête ce modèle.

C'est exactement ainsi que l'IAR IDE détermine la quantité de pile utilisée.

32
Michael Burr

Vous avez marqué votre question avec l'analyse statique, mais c'est un problème difficile à résoudre par l'analyse statique. L'utilisation de la pile dépend du profil d'exécution du programme, en particulier si vous utilisez la récursivité ou l'allocation. Étant donné qu'il s'agit d'une plate-forme intégrée, je suppose qu'il est également difficile d'exécuter quelque chose comme ps ou top et voyez combien de pile votre application utilise.

Une approche intéressante consiste à utiliser l'adresse de la trame de pile actuelle afin de déterminer la quantité de pile utilisée. Vous pouvez le faire en prenant l'adresse de l'argument d'une fonction ou d'une variable locale. Faites cela pour la fonction principale et pour les fonctions qui, selon vous, utilisent le plus de pile. La différence vous indiquera la quantité de pile dont votre application a besoin. Voici un exemple (en supposant une croissance de pile haute à basse habituelle).

char *stack_top, stack_bottom;

int
main(int argc, char *argv[])
{
    stack_top = (char *)&argc;
    // ...
    printf("Stack usage: %d\n", stack_top - stack_bottom);
}

void
deeply_nested_function(void)
{
    int a;
    stack_bottom = (char *)&a;
    // ...
}

Si votre compilateur vous permet de spécifier un prologue de fonction personnalisé (beaucoup le font pour permettre le profilage de programme basé sur des graphiques), vous pouvez même arranger toutes les fonctions pour appeler un tel code de mesure. Ensuite, votre fonction de mesure devient quelque chose comme

void
stack_measurement_function(void)
{
    int a;
    stack_bottom = min(stack_bottom, (char *)&a);
    // ...
}

J'ai utilisé une approche similaire à ce que j'ai décrit pour générer ces graphiques .

22
Diomidis Spinellis

Avec un bon outil d'analyse statique du code source, vous pouvez produire un graphique d'appel pour votre application. Étant donné que, et les estimations de la quantité de sections locales/temporaires produites par votre compilateur, vous pouvez simplement calculer une estimation prudente de la demande de pile.

Ce que j'entends par "bon" outil d'analyse est celui qui peut lire toutes les unités de compilation impliquées, peut déterminer les appels de fonction directs, peut déterminer les pointeurs indirects, dans l'unité de compilation, peut calculer une analyse point à point conservatrice sur l'ensemble du système, peut construire un graphe d'appel prenant en compte l'analyse des points vers. Cela élimine beaucoup d'outils, c'est pourquoi on voit des méthodes ad hoc telles que "remplir la pile au moment de l'exécution et voir ce qui se passe". Vous avez également besoin d'estimations des demandes de pile que le compilateur place sur la pile; vous pouvez approximer une grande partie de cela en connaissant simplement la taille des demandes de stockage de tous les types, ce qui est généralement assez facile à déterminer pour les programmes C des systèmes embarqués. Enfin, vous devez croire que votre application n'a pas d'appels récursifs, ou que l'outil a une idée de la récursivité la plus profonde (probablement en vous le disant).

Le DMS Software Reengineering Toolkit répond à toutes ces exigences pour les programmes C. Voir http://www.semanticdesigns.com/Products/DMS/DMSToolkit.html Vous devez toujours le configurer pour calculer la demande de pile en analysant le graphique des appels et en utilisant les différentes estimations de taille.

Si vous voulez une réponse rapide, utilisez l'astuce stack-fill. Si vous voulez une réponse que vous pouvez recalculer après chaque changement de code source, vous aurez besoin de l'approche d'analyse statique.

5
Ira Baxter

Je travaille sur ce problème en ce moment - c'est-à-dire le calcul analytique de la taille de pile. Cela va clairement être un morceau de code très récursif, car un appel de fonction pourrait avoir un tableau indexé comme un ou plusieurs de ses arguments et un ou plusieurs des indices de tableau pourraient impliquer des appels de fonction !!

Cependant, quelques réalisations permettent de soulager la complexité:

(1) Lors de l'utilisation d'un compilateur de langage de haut niveau, le pointeur de pile à la fin de l'exécution de chaque instruction/ligne de code doit être au même endroit qu'au début. (Au moins, ce serait une bonne règle à observer sinon vous allez avoir des problèmes!)

(2) Le pointeur de pile après le retour de chaque appel de fonction ou de sous-programme doit être le même qu'il était avant l'appel. Par conséquent, la taille de pile maximale est la valeur maximale, sur toutes les instructions du programme, de la taille de pile maximale atteinte dans chaque instruction. (Au moins, ce serait une bonne règle à observer sinon vous allez avoir des problèmes!)

Bien sûr, une déclaration peut inclure les problèmes récursifs que j'ai mentionnés ci-dessus, mais au moins le problème de trouver l'exigence de taille de pile maximale d'un programme entier se résume alors à trouver l'exigence de taille de pile maximale de chaque déclaration, puis à choisir le maximum de ceux-ci.

Cela ne peut pas être terminé tant que toutes les fonctions appelées n'ont pas été compilées. Je génère donc un fichier pour chaque module compilé qui enregistre un certain nombre de tailles de pile pour chaque instruction (essentiellement la valeur maximale avant chaque appel de fonction et la valeur immédiatement avant chaque appel de fonction (à l'exclusion de tout ajout de taille de pile encore inconnu provoqué par la fonction) et les noms des fonctions impliquées. Ensuite, je traite rétrospectivement ces fichiers en utilisant une routine récursive, une fois que toutes les fonctions ont été compilées, pour déterminer la taille maximale de la pile.

La chance est que, à part les routines récursives, l'exigence de taille de pile maximale possible ne dépend pas du flux de programme, bien que dans un flux typique (qui dépend des données), cette taille de pile maximale possible ne soit jamais atteinte.

Exemple: supposons que la fonction 1 appelle la fonction 2 et que le flux de programme des deux dépend d'une valeur de données X. Supposons qu'il existe une plage de X qui amène la fonction 1 à exécuter sa pire instruction, qui comprend un appel à la fonction 2, qui ne s'exécute pas son pire énoncé pour cette même plage de X. Puisque nous avons calculé la taille de pile maximale possible en utilisant simultanément les pires cas pour la fonction 1 et la fonction 2, nous avons peut-être surestimé la taille de pile. Au moins, nous avons commis une erreur du côté sûr.

J'aime donner aux routines d'interruption leur propre espace de pile sur une pile de système d'exploitation si elles en ont besoin, de sorte qu'elles n'ajoutent pas aux exigences de pile de programme, à l'exception de l'adresse de retour d'interruption

2
cloggervic