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:
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.
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 .
la source
Réponses:
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.
la source
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.)
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):
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 :
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.
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.)
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.
la source
inline static void turnOnFan(void) { PORTAbits &= ~(1<<8); }
qui est appelé dans de nombreux endroits est un candidat parfait.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 inline
c'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;)la source
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:
compilera pour:
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.
la source
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:
Choisir une cible et un compilateur à des fins de démonstration.
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.
les supprime maintenant lorsqu'ils sont en ligne.
Mais la réalité est quand vous prenez le vendeur de puces ou les bibliothèques BSP,
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.
la source
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.
la source
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-programmesinitModule()
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.la source
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 inline
pour le définir:Maintenant, si la fonction est encore un peu plus longue, par exemple sur 3 lignes, il peut être judicieux de l'éviter
static inline
et 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.c
et l'utilisez depuisfile2.c
, le compilateur ne l'inline pas automatiquement. Cependant, si vous le définissez enfile1.h
tant questatic inline
fonction, il est probable que votre compilateur l'inline.Ces
static inline
fonctions 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.la source
file1.c
et l'utilisez depuisfile2.c
, le compilateur ne l'inline pas automatiquement. Faux . Voir par exemple-flto
dans gcc ou clang.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]:
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:
puis appelez ce code en utilisant:
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
icc
sont 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 paricc
n'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:
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.
Une implémentation de qualité peut traiter tous les accès aux
volatile
objets comme s'ils pouvaient déclencher des actions qui accéderaient à n'importe quel objet visible par le monde extérieur.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
volatile
comme 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.
la source
volatile
accè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.buff
n'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) commevolatile
. Le contenu de l'buff
accès est-il dans un gestionnaire d'interruption? Oui. Alors ça devrait l'êtrevolatile
.magic_write_count
est zéro, le stockage appartient à la ligne principale. Lorsqu'il est différent de zéro, il appartient au gestionnaire d'interruption. Rendrebuff
volatile nécessiterait que toutes les fonctions qui opèrent sur lui utilisent desvolatile
pointeurs qualifiés, ce qui nuirait beaucoup plus à l'optimisation que d'avoir un compilateur ...