Quelle est la meilleure approche lors de l'écriture de fonctions pour des logiciels embarqués afin d'obtenir de meilleures performances? [fermé]

13

J'ai vu certaines bibliothèques pour microcontrôleurs et leurs fonctions faire une chose à la fois. Par exemple, quelque chose comme ceci:

void setCLK()
{
    // Code to set the clock
}

void setConfig()
{
    // Code to set the config
}

void setSomethingElse()
{
   // 1 line code to write something to a register.
}

Viennent ensuite d'autres fonctions qui utilisent ce code d'une ligne contenant une fonction à d'autres fins. Par exemple:

void initModule()
{
   setCLK();
   setConfig();
   setSomethingElse();
}

Je ne suis pas sûr, mais je crois que de cette façon, cela créerait plus d'appels aux sauts et créerait une surcharge d'empilement des adresses de retour chaque fois qu'une fonction est appelée ou quittée. Et cela rendrait le programme lent, non?

J'ai cherché et partout ils disent que la règle générale de la programmation est qu'une fonction ne doit effectuer qu'une seule tâche.

Donc, si j'écris directement un module fonction InitModule qui règle l'horloge, ajoute la configuration souhaitée et fait autre chose sans appeler de fonctions. Est-ce une mauvaise approche lors de l'écriture de logiciels embarqués?


EDIT 2:

  1. Il semble que beaucoup de gens ont compris cette question comme si j'essayais d'optimiser un programme. Non, je n'ai aucune intention de le faire . Je laisse le compilateur le faire, car ce sera toujours (j'espère pas!) Meilleur que moi.

  2. Tous les blâmes pour moi d'avoir choisi un exemple qui représente du code d'initialisation . La question n'a pas l'intention de prendre en compte les appels de fonction effectués à des fins d'initialisation. Ma question est la suivante : est -ce que la division d'une certaine tâche en petites fonctions multi-lignes ( donc en ligne est hors de question ) s'exécutant dans une boucle infinie a un avantage sur l'écriture d'une fonction longue sans fonction imbriquée?

Veuillez considérer la lisibilité définie dans la réponse de @Jonk .

MaNyYaCk
la source
28
Vous êtes très naïf (ce n'est pas une insulte) si vous pensez qu'un compilateur raisonnable transformera aveuglément le code écrit en binaires comme écrit. La plupart des compilateurs modernes sont assez bons pour identifier quand une routine est mieux intégrée et même quand un emplacement registre vs RAM doit être utilisé pour contenir une variable. Suivez les deux règles d'optimisation: 1) n'optimisez pas. 2) n'optimisez pas encore . Rendez votre code lisible et maintenable et ALORS seulement après avoir profilé un système qui fonctionne, cherchez à l'optimiser.
akohlsmith
10
@akohlsmith IIRC les trois règles d'optimisation sont: 1) Ne faites pas! 2) Non vraiment pas! 3) Profilez d'abord, puis optimisez ensuite si vous devez - Michael_A._Jackson
esoterik
3
N'oubliez pas que «l'optimisation prématurée est la racine de tout mal (ou du moins la plupart) dans la programmation» - Knuth
Mawg dit de réintégrer Monica
1
@Mawg: Le mot clé est prématuré . (Comme l'explique le paragraphe suivant de cet article. Littéralement la phrase suivante: "Pourtant, nous ne devons pas laisser passer nos opportunités dans ces 3% critiques.") jusqu'à ce que vous ayez quelque chose à profiler - mais ne vous engagez pas non plus dans la pessimisation, par exemple en utilisant des outils manifestement incorrects pour le travail.
cHao
1
@Mawg Je ne sais pas pourquoi j'ai obtenu des réponses / commentaires concernant l'optimisation, car je n'ai jamais mentionné le mot et j'ai l'intention de le faire. La question est beaucoup plus sur la façon d'écrire des fonctions dans la programmation intégrée pour obtenir de meilleures performances.
MaNyYaCk

Réponses:

28

Sans doute, dans votre exemple, les performances n'auraient pas d'importance, car le code n'est exécuté qu'une seule fois au démarrage.

Une règle d'or que j'utilise: Écrivez votre code aussi lisible que possible et ne commencez à optimiser que si vous remarquez que votre compilateur ne fait pas correctement sa magie.

Le coût d'un appel de fonction dans un ISR peut être le même que celui d'un appel de fonction lors du démarrage en termes de stockage et de synchronisation. Cependant, les exigences de synchronisation pendant cet ISR pourraient être beaucoup plus critiques.

En outre, comme d'autres l'ont déjà remarqué, le coût (et la signification du «coût») d'un appel de fonction diffère selon la plate-forme, le compilateur, le paramètre d'optimisation du compilateur et les exigences de l'application. Il y aura une énorme différence entre un 8051 et un cortex-m7, et un stimulateur cardiaque et un interrupteur d'éclairage.

Lanting
la source
6
OMI le deuxième paragraphe devrait être en gras et en haut. Il n'y a rien de mal à choisir les bons algorithmes et structures de données dès le départ, mais s'inquiéter de la surcharge des appels de fonction à moins que vous n'ayez découvert qu'il s'agit d'un véritable goulot d'étranglement est définitivement une optimisation prématurée et devrait être évité.
Fund Monica's Lawsuit
11

Il n'y a aucun avantage auquel je peux penser (mais voir la note à JasonS en bas), encapsulant une ligne de code comme une fonction ou un sous-programme. Sauf peut-être que vous pouvez nommer la fonction quelque chose de «lisible». Mais vous pouvez tout aussi bien commenter la ligne. Et comme le fait d'encapsuler une ligne de code dans une fonction coûte de la mémoire de code, de l'espace de pile et du temps d'exécution, il me semble que c'est surtout contre-productif. En situation d'enseignement? Cela pourrait avoir un certain sens. Mais cela dépend de la classe d'élèves, de leur préparation préalable, du programme et de l'enseignant. Surtout, je pense que ce n'est pas une bonne idée. Mais c'est mon opinion.

Ce qui nous amène à l'essentiel. Votre vaste domaine de questions est, depuis des décennies, un sujet de débat et reste à ce jour un sujet de débat. Donc, au moins en lisant votre question, il me semble que c'est une question d'opinion (comme vous l'avez posée).

Il pourrait être détourné d'être aussi fondé sur l'opinion qu'il l'est, si vous deviez être plus détaillé sur la situation et décrire soigneusement les objectifs que vous teniez comme principaux. Mieux vous définissez vos outils de mesure, plus les réponses peuvent être objectives.


D'une manière générale, vous souhaitez effectuer les opérations suivantes pour tout codage. (Pour ci-dessous, je suppose que nous comparons différentes approches qui atteignent toutes les objectifs. De toute évidence, tout code qui ne parvient pas à effectuer les tâches nécessaires est pire que le code qui réussit, quelle que soit la façon dont il est écrit.)

  1. Soyez cohérent dans votre approche, afin qu'une autre lecture de votre code puisse développer une compréhension de votre approche du processus de codage. Être incohérent est probablement le pire crime possible. Non seulement cela rend la tâche difficile pour les autres, mais cela rend difficile pour vous de revenir au code des années plus tard.
  2. Dans la mesure du possible, essayez d'arranger les choses afin que l'initialisation des différentes sections fonctionnelles puisse être effectuée sans égard à la commande. Lorsque la commande est requise, si elle est due à un couplage étroit de deux sous-fonctions hautement liées, envisagez une seule initialisation pour les deux afin qu'elle puisse être réorganisée sans causer de dommages. Si cela n'est pas possible, documentez l'exigence de commande d'initialisation.
  3. Si possible, encapsulez les connaissances dans un seul endroit. Les constantes ne doivent pas être dupliquées partout dans le code. Les équations qui résolvent une variable doivent exister en un et un seul endroit. Etc. Si vous vous retrouvez à copier et coller un ensemble de lignes qui exécutent certains comportements nécessaires à divers endroits, envisagez un moyen de capturer ces connaissances en un seul endroit et de les utiliser si nécessaire. Par exemple, si vous avez une structure d'arbre qui doit être parcouru d'une manière spécifique, ne pasrépliquez le code d'arborescence à chaque endroit où vous devez parcourir les nœuds de l'arborescence. Au lieu de cela, capturez la méthode de marche dans les arbres en un seul endroit et utilisez-la. De cette façon, si l'arbre change et que la méthode de marche change, vous n'avez qu'un seul endroit à vous soucier et tout le reste du code "fonctionne bien".
  4. Si vous étalez toutes vos routines sur une énorme feuille de papier plate, avec des flèches les reliant comme elles sont appelées par d'autres routines, vous verrez dans chaque application qu'il y aura des "grappes" de routines qui ont beaucoup, beaucoup de flèches entre eux mais seulement quelques flèches en dehors du groupe. Il y aura donc des limites naturelles de routines étroitement couplées et des connexions faiblement couplées entre d'autres groupes de routines étroitement couplées. Utilisez ce fait pour organiser votre code en modules. Cela limitera considérablement la complexité apparente de votre code.

Ce qui précède est généralement vrai pour tous les codages. Je n'ai pas discuté de l'utilisation des paramètres, des variables globales locales ou statiques, etc. La raison en est que pour la programmation intégrée, l'espace d'application impose souvent de nouvelles contraintes extrêmes et très importantes et il est impossible de les discuter toutes sans discuter de chaque application intégrée. Et cela ne se produit pas ici, de toute façon.

Ces contraintes peuvent être n'importe lesquelles (et plus):

  • Limitations de coûts sévères nécessitant des microcontrôleurs extrêmement primitifs avec une mémoire RAM minuscule et presque aucun nombre de broches d'E / S. Pour ceux-ci, de nouveaux ensembles de règles entiers s'appliquent. Par exemple, vous devrez peut-être écrire du code d'assembly car il n'y a pas beaucoup d'espace de code. Vous devrez peut-être utiliser UNIQUEMENT des variables statiques car l'utilisation de variables locales est trop coûteuse et prend trop de temps. Vous devrez peut-être éviter l'utilisation excessive de sous-programmes car (par exemple, certaines parties Microchip PIC), il n'y a que 4 registres matériels dans lesquels stocker les adresses de retour des sous-programmes. Il vous faudra donc peut-être "aplatir" considérablement votre code. Etc.
  • Limitations de puissance sévères nécessitant un code soigneusement conçu pour démarrer et arrêter la plupart des microcontrôleurs et imposant de sérieuses limitations sur le temps d'exécution du code lorsqu'il fonctionne à pleine vitesse. Encore une fois, cela peut parfois nécessiter un codage d'assemblage.
  • Exigences de calendrier sévères. Par exemple, il y a des moments où j'ai dû m'assurer que la transmission d'un 0 à drain ouvert devait prendre EXACTEMENT le même nombre de cycles que la transmission d'un 1. Et que l'échantillonnage de cette même ligne devait également être effectué avec une phase relative exacte à ce moment. Cela signifiait que C ne pouvait pas être utilisé ici. La SEULE manière possible de faire cette garantie est d'élaborer soigneusement le code d'assemblage. (Et même alors, pas toujours sur tous les modèles ALU.)

Etc. (Le code de câblage pour l'instrumentation médicale vitale a également tout un monde.)

Le résultat ici est que le codage intégré n'est souvent pas gratuit pour tous, où vous pouvez coder comme vous le feriez sur un poste de travail. Il existe souvent des raisons de concurrence sévères pour une grande variété de contraintes très difficiles. Et ceux - ci peuvent fortement argumenter contre les plus traditionnels et actions réponses.


En ce qui concerne la lisibilité, je trouve que le code est lisible s'il est écrit d'une manière cohérente que je peux apprendre en le lisant. Et là où il n'y a pas de tentative délibérée d'obscurcir le code. Il n'y a vraiment pas besoin de beaucoup plus.

Le code lisible peut être assez efficace et il peut répondre à toutes les exigences ci-dessus que j'ai déjà mentionnées. L'essentiel est que vous compreniez parfaitement ce que chaque ligne de code que vous écrivez produit au niveau de l'assemblage ou de la machine, au fur et à mesure que vous la codez. C ++ impose une lourde charge au programmeur ici car il existe de nombreuses situations où des extraits de code C ++ identiques génèrent en fait différents extraits de code machine qui ont des performances très différentes. Mais C, en général, est surtout un langage «ce que vous voyez est ce que vous obtenez». C'est donc plus sûr à cet égard.


EDIT per JasonS:

J'utilise C depuis 1978 et C ++ depuis environ 1987 et j'ai beaucoup d'expérience en utilisant les deux pour les ordinateurs centraux, les mini-ordinateurs et (principalement) les applications intégrées.

Jason fait un commentaire sur l'utilisation de "inline" comme modificateur. (À mon avis, il s'agit d'une capacité relativement "nouvelle" car elle n'existait tout simplement pas pendant la moitié de ma vie ou plus en utilisant C et C ++.) L'utilisation de fonctions en ligne peut en fait effectuer de tels appels (même pour une ligne de code) assez pratique. Et c'est beaucoup mieux, si possible, que d'utiliser une macro en raison du typage que le compilateur peut appliquer.

Mais il y a aussi des limites. La première est que vous ne pouvez pas compter sur le compilateur pour "prendre l'indice". Cela peut ou non. Et il y a de bonnes raisons de ne pas en tenir compte. (Pour un exemple évident, si l'adresse de la fonction est prise, cela nécessite l'instanciation de la fonction et l'utilisation de l'adresse pour passer l'appel nécessitera ... un appel. Le code ne peut pas être inséré alors.) Il y a d'autres raisons aussi. Les compilateurs peuvent avoir une grande variété de critères en fonction desquels ils jugent comment gérer l'indice. Et en tant que programmeur, cela signifie que vous devezpasser du temps à apprendre sur cet aspect du compilateur, sinon vous êtes susceptible de prendre des décisions basées sur des idées erronées. Cela ajoute donc un fardeau à la fois à l'auteur du code et à tout lecteur et à toute personne qui envisage de porter le code sur un autre compilateur.

De plus, les compilateurs C et C ++ prennent en charge la compilation séparée. Cela signifie qu'ils peuvent compiler un morceau de code C ou C ++ sans compiler aucun autre code associé pour le projet. Pour incorporer du code, en supposant que le compilateur puisse choisir de le faire autrement, non seulement il doit avoir la déclaration "in scope" mais il doit aussi avoir la définition. Habituellement, les programmeurs veilleront à ce que ce soit le cas s'ils utilisent «en ligne». Mais il est facile pour les erreurs de s'infiltrer.

En général, bien que j'utilise également inline là où je pense que cela est approprié, j'ai tendance à supposer que je ne peux pas m'y fier. Si la performance est une exigence importante, et je pense que le PO a déjà clairement écrit qu'il y a eu un impact significatif sur les performances lorsqu'ils sont passés à une voie plus "fonctionnelle", alors je choisirais certainement d'éviter de compter sur l'inline comme pratique de codage et suivrait à la place un modèle d'écriture de code légèrement différent, mais entièrement cohérent.

Une note finale sur «en ligne» et les définitions «en portée» pour une étape de compilation séparée. Il est possible (pas toujours fiable) que le travail soit effectué au stade de la liaison. Cela peut se produire si et seulement si un compilateur C / C ++ enfouit suffisamment de détails dans les fichiers objets pour permettre à un éditeur de liens d'agir sur les demandes "en ligne". Personnellement, je n'ai pas connu de système de liaison (en dehors de Microsoft) qui prend en charge cette capacité. Mais cela peut arriver. Encore une fois, la question de savoir si elle doit être invoquée ou non dépendra des circonstances. Mais je suppose généralement que cela n'a pas été pelleté sur l'éditeur de liens, sauf si je le sais autrement sur la base de bonnes preuves. Et si je m'y fie, cela sera documenté à un endroit bien en vue.


C ++

Pour ceux qui sont intéressés, voici un exemple de la raison pour laquelle je reste assez prudent en C ++ lors du codage des applications embarquées, malgré sa disponibilité immédiate aujourd'hui. Je vais jeter quelques termes que je pense que tous les programmeurs C ++ embarqués doivent connaître à froid :

  • spécialisation de modèle partielle
  • vtables
  • objet de base virtuel
  • cadre d'activation
  • cadre d'activation se détendre
  • l'utilisation de pointeurs intelligents dans les constructeurs, et pourquoi
  • optimisation de la valeur de retour

Ce n'est qu'une courte liste. Si vous ne savez pas déjà tout sur ces termes et pourquoi je les ai énumérés (et bien d'autres que je n'ai pas énumérés ici), je déconseille l'utilisation de C ++ pour le travail intégré, à moins que ce ne soit pas une option pour le projet .

Jetons un coup d'œil à la sémantique des exceptions C ++ pour obtenir juste une saveur.

AB

A

   .
   .
   foo ();
   String s;
   foo ();
   .
   .

A

B

Le compilateur C ++ voit le premier appel à foo () et peut simplement permettre à une séquence d'activation normale de se dérouler si foo () lève une exception. En d'autres termes, le compilateur C ++ sait qu'aucun code supplémentaire n'est nécessaire à ce stade pour prendre en charge le processus de déroulement de trame impliqué dans la gestion des exceptions.

Mais une fois que String s a été créé, le compilateur C ++ sait qu'il doit être correctement détruit avant qu'un déroulement de trame puisse être autorisé, si une exception se produit plus tard. Ainsi, le deuxième appel à foo () est sémantiquement différent du premier. Si le 2ème appel à foo () lève une exception (ce qu'il peut ou non faire), le compilateur doit avoir placé du code conçu pour gérer la destruction de String s avant de laisser se dérouler le cadre habituel. Ceci est différent du code requis pour le premier appel à foo ().

(Il est possible d'ajouter des décorations supplémentaires en C ++ pour limiter ce problème. Mais le fait est que les programmeurs utilisant C ++ doivent simplement être bien plus conscients des implications de chaque ligne de code qu'ils écrivent.)

Contrairement au malloc de C, le nouveau C ++ utilise des exceptions pour signaler quand il ne peut pas effectuer d'allocation de mémoire brute. Il en sera de même pour «dynamic_cast». (Voir 3ème éd. De Stroustrup, Le langage de programmation C ++, pages 384 et 385 pour les exceptions standard en C ++.) Les compilateurs peuvent autoriser ce comportement à être désactivé. Mais en général, vous encourrez des frais généraux en raison des prologues et des épilogues de gestion des exceptions correctement formés dans le code généré, même lorsque les exceptions n'ont pas lieu et même lorsque la fonction en cours de compilation n'a pas de blocs de gestion des exceptions. (Stroustrup l'a déploré publiquement.)

Sans spécialisation partielle des modèles (tous les compilateurs C ++ ne le prennent pas en charge), l'utilisation de modèles peut entraîner un désastre pour la programmation intégrée. Sans cela, la prolifération de code est un risque sérieux qui pourrait tuer un projet intégré de petite mémoire en un éclair.

Lorsqu'une fonction C ++ renvoie un objet, un compilateur temporaire sans nom est créé et détruit. Certains compilateurs C ++ peuvent fournir du code efficace si un constructeur d'objet est utilisé dans l'instruction return, au lieu d'un objet local, réduisant ainsi les besoins de construction et de destruction d'un objet. Mais tous les compilateurs ne le font pas et de nombreux programmeurs C ++ ne sont même pas au courant de cette «optimisation de la valeur de retour».

Fournir un constructeur d'objet avec un seul type de paramètre peut permettre au compilateur C ++ de trouver un chemin de conversion entre deux types de manière complètement inattendue pour le programmeur. Ce type de comportement "intelligent" ne fait pas partie de C.

Une clause catch spécifiant un type de base "découpera" un objet dérivé levé, car l'objet levé est copié en utilisant le "type statique" de la clause catch et non le "type dynamique" de l'objet. Une source non rare de misère d'exception (lorsque vous sentez que vous pouvez même vous permettre des exceptions dans votre code intégré.)

Les compilateurs C ++ peuvent générer automatiquement des constructeurs, des destructeurs, des constructeurs de copie et des opérateurs d'affectation pour vous, avec des résultats inattendus. Il faut du temps pour se familiariser avec les détails de cela.

Le passage de tableaux d'objets dérivés à une fonction acceptant des tableaux d'objets de base génère rarement des avertissements du compilateur mais donne presque toujours un comportement incorrect.

Étant donné que C ++ n'invoque pas le destructeur d'objets partiellement construits lorsqu'une exception se produit dans le constructeur d'objet, la gestion des exceptions dans les constructeurs requiert généralement des "pointeurs intelligents" afin de garantir que les fragments construits dans le constructeur sont correctement détruits si une exception s'y produit. . (Voir Stroustrup, page 367 et 368.) Il s'agit d'un problème courant dans l'écriture de bonnes classes en C ++, mais bien sûr évité en C car C n'a pas la sémantique de construction et de destruction intégrée. Écriture du code approprié pour gérer la construction des sous-objets dans un objet signifie écrire du code qui doit faire face à ce problème sémantique unique en C ++; en d'autres termes "écrire autour" des comportements sémantiques C ++.

C ++ peut copier des objets passés aux paramètres d'objet. Par exemple, dans les fragments suivants, l'appel "rA (x);" peut amener le compilateur C ++ à invoquer un constructeur pour le paramètre p, afin d'appeler ensuite le constructeur de copie pour transférer l'objet x au paramètre p, puis un autre constructeur pour l'objet de retour (un temporaire sans nom) de la fonction rA, qui bien sûr est copié du paramètre p. Pire, si la classe A a ses propres objets qui ont besoin de construction, cela peut se télescoper de façon désastreuse. (Le programmeur AC éviterait la plupart de ces ordures, l'optimisation manuelle étant donné que les programmeurs C n'ont pas une telle syntaxe pratique et doivent exprimer tous les détails un par un.)

    class A {...};
    A rA (A p) { return p; }
    // .....
    { A x; rA(x); }

Enfin, une petite note pour les programmeurs C. longjmp () n'a pas de comportement portable en C ++. (Certains programmeurs C utilisent cela comme une sorte de mécanisme "d'exception".) Certains compilateurs C ++ tentent en fait de régler les choses à nettoyer lorsque le longjmp est utilisé, mais ce comportement n'est pas portable en C ++. Si le compilateur nettoie les objets construits, il n'est pas portable. Si le compilateur ne les nettoie pas, les objets ne sont pas détruits si le code quitte la portée des objets construits à la suite de longjmp et que le comportement n'est pas valide. (Si l'utilisation de longjmp dans foo () ne laisse pas de portée, alors le comportement peut être correct.) Ce n'est pas trop souvent utilisé par les programmeurs intégrés C mais ils doivent se rendre compte de ces problèmes avant de les utiliser.

jonk
la source
4
Ce type de fonctions utilisées une seule fois n'est jamais compilé comme appel de fonction, le code y est simplement placé sans aucun appel.
Dorian
6
@ Dorian - votre commentaire peut être vrai dans certaines circonstances pour certains compilateurs. Si la fonction est statique dans le fichier, le compilateur a la possibilité de rendre le code en ligne. si elle est visible de l'extérieur, même si elle n'est jamais réellement appelée, il doit y avoir un moyen pour que la fonction puisse être appelée.
uɐɪ
1
@jonk - Une autre astuce que vous n'avez pas mentionnée dans une bonne réponse est d'écrire de simples fonctions de macro qui effectuent l'initialisation ou la configuration sous forme de code en ligne étendu. Ceci est particulièrement utile sur les très petits processeurs où la profondeur d'appel RAM / pile / fonction est limitée.
uɐɪ
@ ʎəʞouɐɪ Oui, j'ai manqué de discuter des macros en C. Celles-ci sont obsolètes en C ++, mais une discussion sur ce point pourrait être utile. Je peux y répondre, si je peux trouver quelque chose d'utile à écrire à ce sujet.
jonk
1
@jonk - Je suis totalement en désaccord avec votre première phrase. Un exemple comme celui inline static void turnOnFan(void) { PORTAbits &= ~(1<<8); }qui est appelé dans de nombreux endroits est un candidat parfait.
Jason S
8

1) Code de lisibilité et de maintenabilité en premier. L'aspect le plus important de toute base de code est qu'elle est bien structurée. Un logiciel bien écrit a tendance à avoir moins d'erreurs. Vous devrez peut-être apporter des modifications dans quelques semaines / mois / années, et cela aide énormément si votre code est agréable à lire. Ou peut-être que quelqu'un d'autre doit faire un changement.

2) La performance du code qui s'exécute une fois n'a pas beaucoup d'importance. Soucieux du style, pas de la performance

3) Même le code dans les boucles serrées doit être correct avant tout. Si vous rencontrez des problèmes de performances, optimisez une fois le code correct.

4) Si vous avez besoin d'optimiser, vous devez mesurer! Ce n'est pas grave si vous pensez ou si quelqu'un vous dit que static inlinec'est juste une recommandation au compilateur. Vous devez regarder ce que fait le compilateur. Vous devez également mesurer si l'intégration a amélioré les performances. Dans les systèmes embarqués, vous devez également mesurer la taille du code, car la mémoire du code est généralement assez limitée. C'est LA règle la plus importante qui distingue l'ingénierie de la conjecture. Si vous ne l'avez pas mesuré, cela n'a pas aidé. L'ingénierie mesure. La science l'écrit;)

Crazor
la source
2
La seule critique que j'ai de votre excellent article par ailleurs est le point 2). Il est vrai que les performances du code d'initialisation ne sont pas pertinentes - mais dans un environnement intégré, la taille peut avoir de l'importance. (Mais cela ne remplace pas le point 1; commencez à optimiser la taille lorsque vous en avez besoin - et pas avant)
Martin Bonner prend en charge Monica
2
Les performances du code d'initialisation peuvent tout d'abord être hors de propos. Lorsque vous ajoutez le mode basse consommation et souhaitez récupérer rapidement pour gérer l'événement de réveil, cela devient pertinent.
berendi - manifestant le
5

Lorsqu'une fonction n'est appelée qu'à un seul endroit (même à l'intérieur d'une autre fonction), le compilateur place toujours le code à cet endroit au lieu d'appeler vraiment la fonction. Si la fonction est appelée à de nombreux endroits, il est logique d'utiliser une fonction au moins du point de vue de la taille du code.

Après avoir compilé le code n'aura pas les appels multiples au lieu de cela la lisibilité sera grandement améliorée.

Vous voudrez également avoir par exemple le code d'initiation ADC dans la même bibliothèque avec d'autres fonctions ADC pas dans le fichier c principal.

De nombreux compilateurs vous permettent de spécifier différents niveaux d'optimisation pour la vitesse ou la taille du code, donc si vous avez une petite fonction appelée à plusieurs endroits, la fonction sera "intégrée", copiée là au lieu d'appeler.

L'optimisation de la vitesse intègrera les fonctions dans le plus d'endroits possible, l'optimisation de la taille du code appellera la fonction, cependant, lorsqu'une fonction n'est appelée qu'à un seul endroit, comme dans votre cas, elle sera toujours "intégrée".

Code comme celui-ci:

function_used_just_once{
   code blah blah;
}
main{
  codeblah;
  function_used_just_once();
  code blah blah blah;
{

compilera pour:

main{
 code blah;
 code blah blah;
 code blah blah blah;
}

sans utiliser aucun appel.

Et la réponse à votre question, dans votre exemple ou similaire, la lisibilité du code n'affecte pas les performances, rien n'est beaucoup en vitesse ou en taille de code. Il est courant d'utiliser plusieurs appels juste pour rendre le code lisible, à la fin, ils seront respectés comme un code en ligne.

Mise à jour pour spécifier que les instructions ci-dessus ne sont pas valides pour les compilateurs de version gratuite paralysés à dessein comme la version gratuite de Microchip XCxx. Ce type d'appels de fonction est une mine d'or pour Microchip pour montrer à quel point la version payante est meilleure et si vous compilez cela, vous trouverez dans l'ASM exactement autant d'appels que vous en avez dans le code C.

De plus, ce n'est pas pour les programmeurs stupides qui s'attendent à utiliser un pointeur vers une fonction intégrée.

Il s'agit de la section électronique, pas de la section C C ++ générale ou de la programmation, la question concerne la programmation des microcontrôleurs où tout compilateur décent fera l'optimisation ci-dessus par défaut.

Veuillez donc arrêter de voter uniquement parce que dans des cas rares et inhabituels, cela pourrait ne pas être vrai.

Dorian
la source
15
Que le code devienne en ligne ou non est un problème spécifique à l'implémentation du fournisseur du compilateur; même l'utilisation du mot-clé en ligne ne garantit pas le code en ligne. C'est un indice pour le compilateur. Les bons compilateurs incorporeront certainement les fonctions utilisées une seule fois s'ils les connaissent. Cependant, il ne le fera généralement pas s'il y a des objets "volatils" dans la portée.
Peter Smith
9
Cette réponse n'est tout simplement pas vraie. Comme le dit @PeterSmith, et selon la spécification du langage C, le compilateur a la possibilité d'inline le code mais peut ne pas, et dans de nombreux cas ne le fera pas. Il y a tellement de compilateurs différents dans le monde pour autant de processeurs cibles différents que faire le genre d'instruction générale dans cette réponse et supposer que tous les compilateurs placeront le code en ligne alors qu'ils n'ont que l'option n'est pas tenable.
u
2
@ ʎəʞouɐɪ Vous pointez de rares cas où ce n'est pas possible et ce serait une mauvaise idée de ne pas appeler une fonction en premier lieu. Je n'ai jamais vu un compilateur aussi stupide pour vraiment utiliser call dans l'exemple simple donné par l'OP.
Dorian
6
Dans les cas où ces fonctions ne sont appelées qu'une seule fois, l'optimisation de l'appel de fonction n'est pratiquement pas un problème. Le système doit-il vraiment récupérer chaque cycle d'horloge lors de la configuration? Comme c'est le cas avec l'optimisation n'importe où - écrivez du code lisible et optimisez uniquement si le profilage montre qu'il est nécessaire .
Baldrickk
5
@MSalters Je ne suis pas concerné par ce que le compilateur finit par faire ici - plus par la façon dont le programmeur l'aborde. La rupture de l'initialisation, comme on le voit dans la question, n'entraîne pas de performances négligeables ou négligeables.
Baldrickk
2

Tout d'abord, il n'y a ni meilleur ni pire; c'est une question d'opinion. Vous avez tout à fait raison, c'est inefficace. Il peut être optimisé ou non; ça dépend. Habituellement, vous verrez ces types de fonctions, horloge, GPIO, minuterie, etc. dans des fichiers / répertoires séparés. Les compilateurs n'ont généralement pas été en mesure d'optimiser ces lacunes. Il y en a un que je connais mais qui n'est pas largement utilisé pour des trucs comme ça.

Un seul fichier:

void dummy (unsigned int);

void setCLK()
{
    // Code to set the clock
    dummy(5);
}

void setConfig()
{
    // Code to set the configuration
    dummy(6);
}

void setSomethingElse()
{
   // 1 line code to write something to a register.
    dummy(7);
}

void initModule()
{
   setCLK();
   setConfig();
   setSomethingElse();
}

Choisir une cible et un compilateur à des fins de démonstration.

Disassembly of section .text:

00000000 <setCLK>:
   0:    e92d4010     push    {r4, lr}
   4:    e3a00005     mov    r0, #5
   8:    ebfffffe     bl    0 <dummy>
   c:    e8bd4010     pop    {r4, lr}
  10:    e12fff1e     bx    lr

00000014 <setConfig>:
  14:    e92d4010     push    {r4, lr}
  18:    e3a00006     mov    r0, #6
  1c:    ebfffffe     bl    0 <dummy>
  20:    e8bd4010     pop    {r4, lr}
  24:    e12fff1e     bx    lr

00000028 <setSomethingElse>:
  28:    e92d4010     push    {r4, lr}
  2c:    e3a00007     mov    r0, #7
  30:    ebfffffe     bl    0 <dummy>
  34:    e8bd4010     pop    {r4, lr}
  38:    e12fff1e     bx    lr

0000003c <initModule>:
  3c:    e92d4010     push    {r4, lr}
  40:    e3a00005     mov    r0, #5
  44:    ebfffffe     bl    0 <dummy>
  48:    e3a00006     mov    r0, #6
  4c:    ebfffffe     bl    0 <dummy>
  50:    e3a00007     mov    r0, #7
  54:    ebfffffe     bl    0 <dummy>
  58:    e8bd4010     pop    {r4, lr}
  5c:    e12fff1e     bx    lr

C'est ce que la plupart des réponses ici vous disent, que vous êtes naïf et que tout cela est optimisé et que les fonctions sont supprimées. Eh bien, ils ne sont pas supprimés car ils sont définis globalement par défaut. Nous pouvons les supprimer s'ils ne sont pas nécessaires en dehors de ce fichier.

void dummy (unsigned int);

static void setCLK()
{
    // Code to set the clock
    dummy(5);
}

static void setConfig()
{
    // Code to set the configuration
    dummy(6);
}

static void setSomethingElse()
{
   // 1 line code to write something to a register.
    dummy(7);
}

void initModule()
{
   setCLK();
   setConfig();
   setSomethingElse();
}

les supprime maintenant lorsqu'ils sont en ligne.

Disassembly of section .text:

00000000 <initModule>:
   0:    e92d4010     push    {r4, lr}
   4:    e3a00005     mov    r0, #5
   8:    ebfffffe     bl    0 <dummy>
   c:    e3a00006     mov    r0, #6
  10:    ebfffffe     bl    0 <dummy>
  14:    e3a00007     mov    r0, #7
  18:    ebfffffe     bl    0 <dummy>
  1c:    e8bd4010     pop    {r4, lr}
  20:    e12fff1e     bx    lr

Mais la réalité est quand vous prenez le vendeur de puces ou les bibliothèques BSP,

Disassembly of section .text:

00000000 <_start>:
   0:    e3a0d902     mov    sp, #32768    ; 0x8000
   4:    eb000010     bl    4c <initModule>
   8:    eafffffe     b    8 <_start+0x8>

0000000c <dummy>:
   c:    e12fff1e     bx    lr

00000010 <setCLK>:
  10:    e92d4010     push    {r4, lr}
  14:    e3a00005     mov    r0, #5
  18:    ebfffffb     bl    c <dummy>
  1c:    e8bd4010     pop    {r4, lr}
  20:    e12fff1e     bx    lr

00000024 <setConfig>:
  24:    e92d4010     push    {r4, lr}
  28:    e3a00006     mov    r0, #6
  2c:    ebfffff6     bl    c <dummy>
  30:    e8bd4010     pop    {r4, lr}
  34:    e12fff1e     bx    lr

00000038 <setSomethingElse>:
  38:    e92d4010     push    {r4, lr}
  3c:    e3a00007     mov    r0, #7
  40:    ebfffff1     bl    c <dummy>
  44:    e8bd4010     pop    {r4, lr}
  48:    e12fff1e     bx    lr

0000004c <initModule>:
  4c:    e92d4010     push    {r4, lr}
  50:    ebffffee     bl    10 <setCLK>
  54:    ebfffff2     bl    24 <setConfig>
  58:    ebfffff6     bl    38 <setSomethingElse>
  5c:    e8bd4010     pop    {r4, lr}
  60:    e12fff1e     bx    lr

Vous allez très certainement commencer à ajouter des frais généraux, ce qui a un coût notable pour les performances et l'espace. Quelques à cinq pour cent de chacun selon la taille de chaque fonction.

Pourquoi est-ce fait de toute façon? Il s'agit en partie de l'ensemble de règles que les professeurs enseignent ou enseignent encore pour faciliter la notation du code. Les fonctions doivent tenir sur une page (en arrière lorsque vous avez imprimé votre travail sur papier), ne faites pas ceci, ne faites pas cela, etc. Il s'agit en grande partie de créer des bibliothèques avec des noms communs pour différentes cibles. Si vous avez des dizaines de familles de microcontrôleurs, dont certaines partagent des périphériques et d'autres non, peut-être trois ou quatre saveurs UART différentes mélangées dans les familles, différents GPIO, contrôleurs SPI, etc. Vous pouvez avoir une fonction générique gpio_init (), get_timer_count (), etc. Et réutilisez ces abstractions pour les différents périphériques.

Cela devient un cas de maintenance et de conception de logiciels, avec une lisibilité possible. Maintenabilité, lisibilité et performances que vous ne pouvez pas avoir tout; vous ne pouvez en choisir qu'un ou deux à la fois, pas les trois.

Il s'agit essentiellement d'une question fondée sur l'opinion, et ce qui précède montre les trois principales façons de procéder. En ce qui concerne le meilleur chemin qui est strictement l'opinion. Est-ce que tout le travail est effectué dans une seule fonction? Une question basée sur l'opinion, certains préfèrent les performances, certains définissent la modularité et leur version de lisibilité comme MEILLEURE. La question intéressante de ce que beaucoup de gens appellent la lisibilité est extrêmement douloureuse; pour "voir" le code, vous devez avoir 50 à 10 000 fichiers ouverts à la fois et essayer en quelque sorte de voir linéairement les fonctions dans l'ordre d'exécution pour voir ce qui se passe. Je trouve que c'est le contraire de la lisibilité, mais d'autres le trouvent lisible car chaque élément tient dans la fenêtre écran / éditeur et peut être consommé dans son intégralité après avoir mémorisé les fonctions appelées et / ou avoir un éditeur qui peut entrer et sortir chaque fonction dans un projet.

C'est un autre facteur important lorsque vous voyez différentes solutions. Les éditeurs de texte, les IDE, etc. sont très personnels et vont au-delà de vi vs Emacs. Efficacité de la programmation, les lignes par jour / mois augmentent si vous êtes à l'aise et efficace avec l'outil que vous utilisez. Les fonctionnalités de l'outil peuvent / seront intentionnellement ou non orientées vers la façon dont les fans de cet outil écrivent du code. Et par conséquent, si une personne écrit ces bibliothèques, le projet reflète dans une certaine mesure ces habitudes. Même s'il s'agit d'une équipe, les habitudes / préférences du développeur principal ou du patron peuvent être imposées au reste de l'équipe.

Normes de codage qui ont beaucoup de préférences personnelles enfouies, vi très religieux contre Emacs à nouveau, tabulations contre espaces, comment les parenthèses sont alignées, etc. Et celles-ci jouent dans la façon dont les bibliothèques sont conçues dans une certaine mesure.

Comment devez-vous écrire le vôtre? Comme vous voulez, il n'y a vraiment pas de mauvaise réponse si cela fonctionne. Il y a certes un code mauvais ou risqué, mais s'il est écrit de manière à ce que vous puissiez le maintenir selon vos besoins, il répond à vos objectifs de conception, renonce à la lisibilité et à une certaine maintenabilité si les performances sont importantes, ou vice versa. Aimez-vous les noms de variables courts afin qu'une seule ligne de code s'adapte à la largeur de la fenêtre de l'éditeur? Ou de longs noms trop descriptifs pour éviter toute confusion, mais la lisibilité diminue car vous ne pouvez pas obtenir une ligne sur une page; maintenant, il est visuellement brisé, jouant avec le flux.

Vous n'allez pas frapper un home run la première fois au bâton. Cela peut / devrait prendre des décennies pour vraiment définir votre style. Dans le même temps, au cours de cette période, votre style peut changer, penché dans un sens pendant un certain temps, puis penché dans un autre.

Vous allez entendre beaucoup de choses ne pas optimiser, ne jamais optimiser et optimisation prématurée. Mais comme indiqué, des conceptions comme celle-ci depuis le début créent des problèmes de performances, puis vous commencez à voir des hacks pour résoudre ce problème plutôt que de les reconcevoir dès le début pour les exécuter. Je suis d'accord qu'il y a des situations, une seule fonction quelques lignes de code que vous pouvez essayer de manipuler le compilateur en fonction de la peur de ce que le compilateur va faire autrement (notez avec l'expérience que ce type de codage devient facile et naturel, optimisation au fur et à mesure que vous écrivez en sachant comment le compilateur va compiler le code), puis vous voulez confirmer où se trouve réellement le voleur de cycle, avant de l'attaquer.

Vous devez également concevoir votre code pour l'utilisateur dans une certaine mesure. S'il s'agit de votre projet, vous êtes le seul développeur; c'est tout ce que vous voulez. Si vous essayez de créer une bibliothèque à donner ou à vendre, vous voudrez probablement faire ressembler votre code à toutes les autres bibliothèques, des centaines à des milliers de fichiers avec de minuscules fonctions, des noms de fonction longs et des noms de variables longs. Malgré les problèmes de lisibilité et les problèmes de performances, l'OMI, vous trouverez que plus de gens pourront utiliser ce code.

old_timer
la source
4
Vraiment? Quels «certains cible» et «certains compilateur» utilisez-vous puis-je demander?
Dorian
Cela me ressemble plus à un ARM8 32/64 bits, peut-être d'un PI raspbery puis d'un microcontrôleur habituel. Avez-vous lu la première phrase de la question?
Dorian
Eh bien, le compilateur ne supprime pas les fonctions globales inutilisées, mais l'éditeur de liens le fait. S'il est configuré et utilisé correctement, ils n'apparaîtront pas dans l'exécutable.
berendi - manifestant le
Si quelqu'un se demande quel compilateur peut optimiser les lacunes des fichiers: les compilateurs IAR prennent en charge la compilation multi-fichiers (c'est ainsi qu'ils l'appellent), ce qui permet une optimisation croisée des fichiers. Si vous lancez tous les fichiers c / cpp en une seule fois, vous vous retrouvez avec un exécutable qui contient une seule fonction: main. Les avantages en termes de performances peuvent être assez importants.
Arsenal
3
@Arsenal Bien sûr, gcc prend en charge l'inline, même entre les unités de compilation s'il est appelé correctement. Voir gcc.gnu.org/onlinedocs/gcc/Optimize-Options.html et recherchez l'option -flto.
Peter - Réintègre Monica le
1

Règle très générale - le compilateur peut optimiser mieux que vous. Bien sûr, il y a des exceptions si vous faites des choses très intensives en boucle, mais dans l'ensemble si vous voulez une bonne optimisation pour la vitesse ou la taille du code, choisissez judicieusement votre compilateur.

Dirk Bruere
la source
Malheureusement, c'est vrai pour la plupart des programmeurs aujourd'hui.
Dorian
0

Cela dépend à coup sûr de votre propre style de codage. Une règle générale qui existe est que les noms de variables ainsi que les noms de fonctions doivent être aussi clairs et explicites que possible. Plus vous mettez de sous-appels ou de lignes de code dans une fonction, plus il devient difficile de définir une tâche claire pour cette fonction. Dans votre exemple, vous avez une fonction initModule()qui initialise des trucs et appelle des sous-routines qui définissent ensuite l'horloge ou la configuration . Vous pouvez le constater en lisant simplement le nom de la fonction. Si vous mettez tout le code des sous-programmes initModule()directement dans votre, il devient moins évident de savoir ce que fait réellement la fonction. Mais comme souvent, ce n'est qu'une ligne directrice.

le pape
la source
Merci pour votre réponse. Je pourrais changer de style si nécessaire pour les performances, mais la question ici est la lisibilité du code affecte-t-il les performances?
MaNyYaCk
Un appel de fonction entraînera un appel ou une commande jmp mais c'est un sacrifice négligeable de ressources à mon avis. Si vous utilisez des modèles de conception, vous vous retrouvez parfois avec une douzaine de couches d'appels de fonction avant d'atteindre le code réel coupé.
po.pe
@Humpawumpa - Si vous écrivez pour un microcontrôleur avec seulement 256 ou 64 octets de RAM, alors une douzaine de couches d'appels de fonction n'est pas un sacrifice négligeable, ce n'est tout simplement pas possible
uɐɪ
Oui, mais ce sont deux extrêmes ... généralement, vous avez plus de 256 octets et utilisez moins d'une douzaine de couches - j'espère.
po.pe
0

Si une fonction ne fait vraiment qu'une très petite chose, pensez à la faire static inline.

Ajoutez-le à un fichier d'en-tête au lieu du fichier C et utilisez les mots static inlinepour le définir:

static inline void setCLK()
{
    //code to set the clock
}

Maintenant, si la fonction est encore un peu plus longue, par exemple sur 3 lignes, il peut être judicieux de l'éviter static inlineet de l'ajouter au fichier .c. Après tout, les systèmes embarqués ont une mémoire limitée et vous ne voulez pas trop augmenter la taille du code.

De plus, si vous définissez la fonction dans file1.cet l'utilisez depuis file2.c, le compilateur ne l'inline pas automatiquement. Cependant, si vous le définissez en file1.htant que static inlinefonction, il est probable que votre compilateur l'inline.

Ces static inlinefonctions sont extrêmement utiles dans la programmation haute performance. J'ai constaté qu'ils augmentaient souvent les performances du code d'un facteur supérieur à trois.

juhist
la source
"comme avoir plus de 3 lignes" - le nombre de lignes n'a rien à voir avec cela; le coût inline a tout à voir avec cela. Je pourrais écrire une fonction de 20 lignes qui est parfaite pour l'inline, et une fonction de 3 lignes qui est horrible pour l'inline (par exemple, functionA () qui appelle la fonction B () 3 fois, functionB () qui appelle la fonction C () 3 fois, et quelques autres niveaux).
Jason S
De plus, si vous définissez la fonction dans file1.cet l'utilisez depuis file2.c, le compilateur ne l'inline pas automatiquement. Faux . Voir par exemple -fltodans gcc ou clang.
berendi - manifestant le
0

Une difficulté à essayer d'écrire du code efficace et fiable pour les microcontrôleurs est que certains compilateurs ne peuvent pas gérer certaines sémantiques de manière fiable à moins que le code n'utilise des directives spécifiques au compilateur ou ne désactive de nombreuses optimisations.

Par exemple, si possède un système à cœur unique avec une routine de service d'interruption [exécutée par une minuterie ou autre]:

volatile uint32_t *magic_write_ptr,magic_write_count;
void handle_interrupt(void)
{
  if (magic_write_count)
  {
    magic_write_count--;
    send_data(*magic_write_ptr++)
  }
}

il devrait être possible d'écrire des fonctions pour démarrer une opération d'écriture en arrière-plan ou d'attendre qu'elle se termine:

void wait_for_background_write(void)
{
  while(magic_write_count)
    ;
}
void start_background_write(uint32_t *dat, uint32_t count)
{
  wait_for_background_write();
  background_write_ptr = dat;
  background_write_count = count;
}

puis appelez ce code en utilisant:

uint32_t buff[16];

... write first set of data into buff
start_background_write(buff, 16);
... do some stuff unrelated to buff
wait_for_background_write();

... write second set of data into buff
start_background_write(buff, 16);
... etc.

Malheureusement, avec toutes les optimisations activées, un compilateur "intelligent" comme gcc ou clang décidera qu'il n'y a aucun moyen que le premier ensemble d'écritures puisse avoir un quelconque effet sur l'observable du programme et ils peuvent donc être optimisés. Les compilateurs de qualité comme iccsont moins enclins à le faire si l'acte de définir une interruption et d'attendre la fin implique à la fois des écritures volatiles et des lectures volatiles (comme c'est le cas ici), mais la plate-forme ciblée par iccn'est pas si populaire pour les systèmes embarqués.

La norme ignore délibérément les problèmes de qualité de mise en œuvre, estimant qu'il existe plusieurs façons raisonnables de gérer la construction ci-dessus:

  1. Une implémentation de qualité destinée exclusivement à des domaines comme le calcul de nombres haut de gamme pourrait raisonnablement s'attendre à ce que le code écrit pour ces champs ne contienne pas de constructions comme celles ci-dessus.

  2. Une implémentation de qualité peut traiter tous les accès aux volatileobjets comme s'ils pouvaient déclencher des actions qui accéderaient à n'importe quel objet visible par le monde extérieur.

  3. Une implémentation simple mais de qualité décente destinée à l'utilisation de systèmes embarqués peut traiter tous les appels à des fonctions non marquées "en ligne" comme s'ils pouvaient accéder à tout objet qui a été exposé au monde extérieur, même s'il ne se traite pas volatilecomme décrit dans # 2.

La norme n'essaie pas de suggérer laquelle des approches ci-dessus serait la plus appropriée pour une mise en œuvre de qualité, ni d'exiger que les mises en œuvre "conformes" soient de qualité suffisamment bonne pour être utilisables dans un but particulier. Par conséquent, certains compilateurs comme gcc ou clang nécessitent effectivement que tout code voulant utiliser ce modèle soit compilé avec de nombreuses optimisations désactivées.

Dans certains cas, s'assurer que les fonctions d'E / S se trouvent dans une unité de compilation distincte et qu'un compilateur n'aura pas d'autre choix que de supposer qu'il pourrait accéder à n'importe quel sous-ensemble arbitraire d'objets qui ont été exposés au monde extérieur peut être un minimum raisonnable- of-evils façon d'écrire du code qui fonctionnera de manière fiable avec gcc et clang. Dans de tels cas, cependant, le but n'est pas d'éviter le coût supplémentaire d'un appel de fonction inutile, mais plutôt d'accepter le coût qui devrait être inutile en échange de l'obtention de la sémantique requise.

supercat
la source
«s'assurer que les fonctions d'E / S sont dans une unité de compilation distincte» ... n'est pas un moyen infaillible de prévenir des problèmes d'optimisation comme ceux-ci. Au moins LLVM et je crois que GCC effectuera l'optimisation de l'ensemble du programme dans de nombreux cas, donc pourrait décider d'inclure vos fonctions d'E / S même si elles sont dans une unité de compilation distincte.
Jules
@Jules: Toutes les implémentations ne conviennent pas à l'écriture de logiciels intégrés. La désactivation de l'optimisation de l'ensemble du programme peut être le moyen le moins coûteux de forcer gcc ou clang à se comporter comme une implémentation de qualité appropriée à cet effet.
supercat
@Jules: Une implémentation de meilleure qualité destinée à la programmation embarquée ou de systèmes devrait être configurable pour avoir une sémantique appropriée à cette fin sans avoir à désactiver complètement l'optimisation du programme entier (par exemple en ayant une option pour traiter les volatileaccès comme s'ils pouvaient potentiellement déclencher accès arbitraires à d'autres objets), mais pour une raison quelconque, gcc et clang préfèrent traiter les problèmes de qualité de mise en œuvre comme une invitation à se comporter de façon inutile.
supercat
1
Même les implémentations de «haute qualité» ne corrigeront pas le code bogué. S'il buffn'est pas déclaré volatile, il ne sera pas traité comme une variable volatile, les accès à celui-ci peuvent être réorganisés ou entièrement optimisés s'ils ne sont apparemment pas utilisés plus tard. La règle est simple: marquez toutes les variables accessibles en dehors du flux de programme normal (comme vu par le compilateur) comme volatile. Le contenu de l' buffaccès est-il dans un gestionnaire d'interruption? Oui. Alors ça devrait l'être volatile.
berendi - manifestant le
@berendi: Les compilateurs peuvent offrir des garanties au-delà de ce que la norme exige et des compilateurs de qualité le feront. Une implémentation indépendante de qualité pour l'utilisation de systèmes embarqués permettra aux programmeurs de synthétiser des constructions mutex, ce qui est essentiellement ce que fait le code. Quand magic_write_countest zéro, le stockage appartient à la ligne principale. Lorsqu'il est différent de zéro, il appartient au gestionnaire d'interruption. Rendre buffvolatile nécessiterait que toutes les fonctions qui opèrent sur lui utilisent des volatilepointeurs qualifiés, ce qui nuirait beaucoup plus à l'optimisation que d'avoir un compilateur ...
supercat