Was ist der beste Ansatz beim Schreiben von Funktionen für eingebettete Software, um eine bessere Leistung zu erzielen? [geschlossen]


13

Ich habe einige Bibliotheken für Mikrocontroller gesehen und deren Funktionen tun jeweils eine Sache. Zum Beispiel so etwas:

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.
}

Hinzu kommen weitere Funktionen, die diesen 1-zeiligen Code mit einer Funktion für andere Zwecke verwenden. Beispielsweise:

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

Ich bin mir nicht sicher, aber ich glaube, dass dies jedes Mal, wenn eine Funktion aufgerufen oder beendet wird, mehr Aufrufe von Sprüngen und einen Mehraufwand für das Stapeln der Rücksprungadressen verursachen würde. Und das würde das Programm langsam laufen lassen, oder?

Ich habe gesucht und überall heißt es, dass die Daumenregel der Programmierung lautet, dass eine Funktion nur eine Aufgabe ausführen soll.

Wenn ich also direkt einen InitModule-Funktionsbaustein schreibe, der die Uhr einstellt, eine gewünschte Konfiguration hinzufügt und etwas anderes ausführt, ohne Funktionen aufzurufen. Ist es ein schlechter Ansatz beim Schreiben von eingebetteter Software?


EDIT 2:

  1. Anscheinend haben viele Leute diese Frage so verstanden, als würde ich versuchen, ein Programm zu optimieren. Nein, das habe ich nicht vor . Ich lasse es den Compiler machen, weil es immer besser sein wird (ich hoffe aber nicht!) Als ich.

  2. Ich bin schuld daran, dass ich ein Beispiel ausgewählt habe, das einen Initialisierungscode darstellt . Die Frage hat nicht die Absicht, Funktionsaufrufe zu berücksichtigen, die zu Initialisierungszwecken ausgeführt werden. Meine Frage ist: Hat das Aufteilen einer bestimmten Aufgabe in kleine Funktionen aus mehreren Zeilen ( so dass Inline nicht in Frage kommt ), die in einer Endlosschleife ausgeführt werden, einen Vorteil gegenüber dem Schreiben einer langen Funktion ohne verschachtelte Funktionen?

Bitte beachten Sie die in der Antwort von @Jonk definierte Lesbarkeit .


28
Sie sind sehr naiv (nicht als Beleidigung gedacht), wenn Sie glauben, dass ein vernünftiger Compiler Code, wie er geschrieben wurde, blind in Binärdateien umwandelt. Die meisten modernen Compiler sind recht gut darin zu identifizieren, wann eine Routine besser inline ist und auch wenn ein Register gegen den RAM-Speicherort verwendet werden sollte, um eine Variable zu halten. Befolgen Sie die beiden Optimierungsregeln: 1) Optimieren Sie nicht. 2) nicht optimize noch . Machen Sie Ihren Code lesbar und wartbar, und versuchen Sie dann, ihn zu optimieren, nachdem Sie ein funktionierendes System profiliert haben.
Akohlsmith

10
@akohlsmith IIRC Die drei Optimierungsregeln lauten: 1) Nicht! 2) Nein, wirklich nicht! 3) Profil zuerst, dann und nur dann optimieren , wenn Sie muss - Michael_A._Jackson
esoterik

3
Denken Sie daran , dass „vorzeitige Optimierung ist die Wurzel allen Übels ist (oder zumindest die meisten davon) in der Programmierung“ - Knuth
Mawg sagt wieder einzusetzen Monica

1
@Mawg: Das operative Wort ist dort verfrüht . (Wie der nächste Absatz in diesem Artikel erklärt. Buchstäblich der nächste Satz: "Wir sollten jedoch unsere Chancen in diesen kritischen 3% nicht auslassen.") Optimieren Sie nicht, bis Sie es brauchen - Sie werden das Langsame nicht gefunden haben Bis du etwas zu profilieren hast - aber beschäftige dich auch nicht mit Pessimisierung, indem du zum Beispiel offensichtlich falsche Werkzeuge für den Job verwendest.
CHAO

1
@Mawg Ich weiß nicht, warum ich im Zusammenhang mit der Optimierung Antworten / Feedback erhalten habe, da ich das Wort nie erwähnt habe und beabsichtige, es zu tun. Die Frage ist viel mehr, wie man Funktionen in Embedded Programming schreibt, um eine bessere Leistung zu erzielen.
MaNyYaCk

Antworten:


28

In Ihrem Beispiel spielt die Leistung vermutlich keine Rolle, da der Code beim Start nur einmal ausgeführt wird.

Eine Faustregel, die ich verwende: Schreiben Sie Ihren Code so lesbar wie möglich und optimieren Sie ihn nur, wenn Sie feststellen, dass Ihr Compiler seine Magie nicht richtig ausführt.

Die Kosten eines Funktionsaufrufs in einem ISR können in Bezug auf Speicherung und Zeitplanung dieselben sein wie die eines Funktionsaufrufs während des Starts. Die Timing-Anforderungen während dieses ISR könnten jedoch wesentlich kritischer sein.

Darüber hinaus unterscheiden sich, wie bereits von anderen bemerkt, die Kosten (und die Bedeutung der 'Kosten') eines Funktionsaufrufs je nach Plattform, Compiler, Einstellung der Compileroptimierung und den Anforderungen der Anwendung. Es wird einen großen Unterschied zwischen einem 8051 und einem Cortex-m7 sowie einem Schrittmacher und einem Lichtschalter geben.


6
IMO sollte der zweite Absatz fett und oben sein. Es ist nichts Falsches daran, die richtigen Algorithmen und Datenstrukturen auf Anhieb auszuwählen, aber Sie müssen sich Sorgen über den Funktionsaufruf-Overhead machen, es sei denn, Sie haben festgestellt, dass es sich um einen tatsächlichen Engpass handelt, der definitiv eine vorzeitige Optimierung darstellt und vermieden werden sollte.
Fund Monica's Lawsuit

11

Es gibt keinen Vorteil, den ich mir vorstellen kann (siehe Anmerkung zu JasonS unten), eine Codezeile als Funktion oder Unterroutine aufzuschließen. Außer vielleicht, dass Sie der Funktion einen "lesbaren" Namen geben können. Sie können die Zeile aber genauso gut kommentieren. Und da das Einschließen einer Codezeile in eine Funktion Codespeicher, Stapelspeicher und Ausführungszeit kostet, scheint es mir das meiste zu sein kontraproduktiv ist. In einer Unterrichtssituation? Es könnte einen Sinn ergeben. Das hängt jedoch von der Klasse der Schüler, ihrer Vorbereitung im Voraus, dem Lehrplan und dem Lehrer ab. Meistens denke ich, dass es keine gute Idee ist. Aber das ist meine Meinung.

Das bringt uns zum Endergebnis. Ihr breiter Fragenbereich ist seit Jahrzehnten umstritten und bleibt bis heute umstritten. Zumindest wenn ich Ihre Frage lese, scheint es mir eine meinungsbasierte Frage zu sein (wie Sie sie gestellt haben).

Wenn Sie die Situation detaillierter beschreiben und die Ziele, die Sie als vorrangig erachtet haben, sorgfältig beschreiben, könnte dies dazu führen, dass Sie nicht mehr so ​​meinungsbasiert wie bisher sind. Je besser Sie Ihre Messinstrumente definieren, desto objektiver können die Antworten sein.


Im Großen und Ganzen möchten Sie für jede Codierung Folgendes tun . (Im Folgenden werde ich davon ausgehen, dass wir verschiedene Ansätze vergleichen, mit denen alle Ziele erreicht werden. Offensichtlich ist jeder Code, der die erforderlichen Aufgaben nicht ausführt, schlechter als erfolgreicher Code, unabhängig davon, wie er geschrieben wurde.)

  1. Seien Sie konsequent in Bezug auf Ihren Ansatz, damit ein erneutes Lesen Ihres Codes ein Verständnis dafür entwickeln kann, wie Sie Ihren Codierungsprozess angehen. Inkonsistent zu sein ist wahrscheinlich das schlimmste Verbrechen. Das macht es nicht nur anderen schwer, sondern auch Ihnen, Jahre später zum Code zurückzukehren.
  2. Versuchen Sie nach Möglichkeit, die Dinge so anzuordnen, dass die Initialisierung verschiedener Funktionsabschnitte unabhängig von der Bestellung durchgeführt werden kann. Wo eine Bestellung erforderlich ist, wenn es sich um eine enge Kupplung handelt zweier in hohem Maße verwandter Unterfunktionen eine einzige Initialisierung für beide in Betracht ziehen, damit die Reihenfolge geändert werden kann, ohne Schaden zu verursachen. Wenn dies nicht möglich ist, dokumentieren Sie die Anforderungen für die Initialisierungsreihenfolge.
  3. Encapsulate KnowledgeWenn möglich, an genau einem Ort. Konstanten sollten nicht überall im Code dupliziert werden. Gleichungen, die für eine Variable aufgelöst werden, sollten nur an einer Stelle vorhanden sein. Und so weiter. Wenn Sie feststellen, dass Sie eine Reihe von Zeilen kopieren und einfügen, die an verschiedenen Stellen das erforderliche Verhalten aufweisen, sollten Sie überlegen, wie Sie dieses Wissen an einem Ort erfassen und bei Bedarf verwenden können. Wenn Sie beispielsweise eine Baumstruktur haben, die auf eine bestimmte Weise begangen werden muss, tun Sie dies nichtReplizieren Sie den Tree-Walking-Code an jeder Stelle, an der Sie die Baumknoten durchlaufen müssen. Erfassen Sie stattdessen die Tree-Walking-Methode an einem Ort und verwenden Sie sie. Auf diese Weise haben Sie nur einen Grund zur Sorge, wenn sich der Baum und die Laufmethode ändern, und der gesamte Rest des Codes funktioniert einfach.
  4. Wenn Sie alle Ihre Routinen auf ein großes, flaches Blatt Papier verteilen und die Pfeile so verbinden, wie sie von anderen Routinen aufgerufen werden, werden Sie in jeder Anwendung feststellen, dass es "Cluster" von Routinen mit vielen, vielen Pfeilen gibt untereinander aber nur wenige pfeile ausserhalb der gruppe. Es wird also natürliche Grenzen eng gekoppelter Routinen und lose gekoppelter Verbindungen zwischen anderen Gruppen eng gekoppelter Routinen geben. Verwenden Sie diese Tatsache, um Ihren Code in Module zu organisieren. Dies wird die scheinbare Komplexität Ihres Codes erheblich einschränken.

Das oben Gesagte gilt generell für alle Codierungen. Ich habe die Verwendung von Parametern, lokalen oder statischen globalen Variablen usw. nicht erörtert. Der Grund dafür ist, dass der Anwendungsbereich für die eingebettete Programmierung häufig extreme und sehr bedeutende neue Einschränkungen enthält und es unmöglich ist, alle zu erörtern, ohne jede eingebettete Anwendung zu erörtern. Und das passiert hier sowieso nicht.

Diese Einschränkungen können eine oder mehrere der folgenden sein:

  • Starke Kosteneinschränkungen, die extrem einfache MCUs mit minimalem RAM und fast keiner Anzahl von E / A-Pins erfordern. Für diese gelten ganz neue Regeln. Beispielsweise müssen Sie möglicherweise in Assemblycode schreiben, da nicht viel Codespeicherplatz vorhanden ist. Möglicherweise müssen Sie NUR statische Variablen verwenden, da die Verwendung lokaler Variablen zu kostspielig und zeitaufwendig ist. Möglicherweise müssen Sie die übermäßige Verwendung von Unterprogrammen vermeiden, da (z. B. bei einigen Microchip PIC-Teilen) nur 4 Hardwareregister zum Speichern von Unterprogramm-Rücksprungadressen vorhanden sind. Daher müssen Sie Ihren Code möglicherweise dramatisch "reduzieren". Etc.
  • Schwerwiegende Leistungsbeschränkungen erfordern sorgfältig ausgearbeiteten Code, um die meisten MCUs zu starten und herunterzufahren, und beschränken die Ausführungszeit des Codes erheblich, wenn er mit voller Geschwindigkeit ausgeführt wird. Wiederum kann dies manchmal eine gewisse Assembler-Codierung erfordern.
  • Strenge Timing-Anforderungen. Zum Beispiel gab es Zeiten, in denen ich sicherstellen musste, dass die Übertragung einer Open-Drain-0 GENAU die gleiche Anzahl von Zyklen wie die Übertragung einer 1 dauern musste. Und dass das Abtasten derselben Leitung auch durchgeführt werden musste mit einer genauen relativen Phase zu diesem Zeitpunkt. Dies bedeutete, dass C hier NICHT verwendet werden konnte. Die EINZIGE Möglichkeit, diese Garantie zu geben, besteht darin, den Montagecode sorgfältig zu erstellen. (Und selbst dann nicht immer bei allen ALU-Designs.)

Und so weiter. (Der Verkabelungscode für lebenswichtige medizinische Instrumente hat auch eine eigene Welt.)

Das Fazit ist, dass eingebettetes Coding oft nicht für alle kostenlos ist und Sie wie auf einer Workstation codieren können. Es gibt oft schwerwiegende Wettbewerbsgründe für eine Vielzahl sehr schwieriger Sachzwänge. Und diese mögen stark gegen die eher traditionellen und gängigen Antworten sprechen .


In Bezug auf die Lesbarkeit finde ich, dass Code lesbar ist, wenn er in einer konsistenten Weise geschrieben ist, die ich beim Lesen lernen kann. Und wo es keinen absichtlichen Versuch gibt, den Code zu verschleiern. Es ist wirklich nicht viel mehr erforderlich.

Lesbarer Code kann sehr effizient sein und alle oben genannten Anforderungen erfüllen. Die Hauptsache ist, dass Sie genau verstehen, was jede von Ihnen geschriebene Codezeile auf Assembly- oder Maschinenebene erzeugt, während Sie sie codieren. C ++ stellt hier eine ernsthafte Belastung für den Programmierer dar, da es viele Situationen gibt, in denen identische C ++ - Code- Snippets tatsächlich verschiedene Code-Snippets erzeugen, die eine sehr unterschiedliche Leistung aufweisen. Aber C ist im Allgemeinen meistens eine "was Sie sehen, ist was Sie bekommen" Sprache. In dieser Hinsicht ist es also sicherer.


EDIT pro JasonS:

Ich benutze C seit 1978 und C ++ seit ungefähr 1987 und ich habe viel Erfahrung mit beiden für Großrechner, Minicomputer und (meistens) eingebettete Anwendungen.

Jason bringt einen Kommentar zur Verwendung von 'inline' als Modifikator vor. (Aus meiner Sicht ist dies eine relativ "neue" Funktion, da sie unter Verwendung von C und C ++ nur für die Hälfte meines Lebens oder länger existierte.) Die Verwendung von Inline-Funktionen kann solche Aufrufe tatsächlich ausführen (sogar für eine Zeile von Code) ganz praktisch. Und es ist weitaus besser, wenn möglich, als ein Makro zu verwenden, da der Compiler die Eingabe anwenden kann.

Es gibt aber auch Einschränkungen. Das erste ist, dass Sie sich nicht darauf verlassen können, dass der Compiler "den Hinweis aufnimmt". Es kann oder kann nicht. Und es gibt gute Gründe, den Hinweis nicht zu verstehen. (Ein offensichtliches Beispiel: Wenn die Adresse der Funktion verwendet wird, muss die Funktion instanziiert werden, und die Verwendung der Adresse zum Tätigen des Anrufs erfordert einen Anruf. Der Code kann dann nicht in die Zeile eingefügt werden.) Es gibt auch aus anderen gründen. Compiler können eine Vielzahl von Kriterien haben, anhand derer sie beurteilen, wie sie mit dem Hinweis umgehen sollen. Und als Programmierer müssen Sie dies tunNehmen Sie sich etwas Zeit, um sich mit diesem Aspekt des Compilers vertraut zu machen. Andernfalls können Sie Entscheidungen treffen, die auf fehlerhaften Ideen beruhen. Dies belastet sowohl den Schreiber des Codes als auch jeden Leser und jeden, der plant, den Code auf einen anderen Compiler zu portieren.

Außerdem unterstützen C- und C ++ - Compiler die separate Kompilierung. Dies bedeutet, dass sie einen Teil des C- oder C ++ - Codes kompilieren können, ohne anderen zugehörigen Code für das Projekt zu kompilieren. Um Code einzufügen, muss der Compiler nicht nur die Deklaration "in scope" haben, sondern auch die Definition. Normalerweise stellen Programmierer sicher, dass dies der Fall ist, wenn sie "Inline" verwenden. Aber es ist leicht, dass sich Fehler einschleichen.

Im Allgemeinen gehe ich davon aus, dass ich mich nicht darauf verlassen kann, obwohl ich Inline auch dort verwende, wo ich es für angemessen halte. Wenn Leistung eine wesentliche Anforderung ist und ich denke, das OP hat bereits klar geschrieben, dass es einen erheblichen Leistungseinbruch gab, als es auf eine "funktionalere" Route ging, dann würde ich es mit Sicherheit vermeiden, mich auf Inline als Codierungspraxis zu verlassen und würde stattdessen einem etwas anderen, aber völlig konsistenten Muster beim Schreiben von Code folgen.

Eine abschließende Anmerkung zu 'Inline' und Definitionen, die für einen separaten Kompilierungsschritt "im Geltungsbereich" sind. Es ist möglich (nicht immer zuverlässig), dass die Arbeit in der Verbindungsphase ausgeführt wird. Dies kann nur dann der Fall sein, wenn ein C / C ++ - Compiler genügend Details in die Objektdateien einfügt, damit ein Linker auf Inline-Anforderungen reagieren kann. Ich persönlich habe kein Linker-System (außerhalb von Microsoft) erlebt, das diese Funktion unterstützt. Aber es kann vorkommen. Auch hier hängt es von den Umständen ab, ob darauf vertraut werden sollte oder nicht. Aber ich gehe normalerweise davon aus, dass dies nicht auf den Linker geschaufelt wurde, sofern ich aufgrund guter Beweise nichts anderes weiß. Und wenn ich mich darauf verlasse, wird es an prominenter Stelle dokumentiert.


C ++

Hier ist ein Beispiel dafür, warum ich beim Codieren von eingebetteten Anwendungen C ++ trotz der sofortigen Verfügbarkeit von C ++ ziemlich vorsichtig bin. Ich werde ein paar Begriffe werfen, dass ich denke , alle Embedded C ++ Programmierer müssen wissen , kalt :

  • Teilschablonenspezialisierung
  • vtables
  • virtuelles Stammobjekt
  • Aktivierungsrahmen
  • Aktivierungsframe abwickeln
  • Verwendung intelligenter Zeiger in Konstruktoren und warum
  • Rückgabewertoptimierung

Das ist nur eine kurze Liste. Wenn Sie noch nicht alles über diese Begriffe wissen und wissen, warum ich sie aufgelistet habe (und viele andere, die ich hier nicht aufgelistet habe), rate ich von der Verwendung von C ++ für eingebettete Arbeit ab, es sei denn, dies ist keine Option für das Projekt .

Werfen wir einen kurzen Blick auf die C ++ - Ausnahmesemantik, um nur einen Vorgeschmack zu erhalten.

EINB

EIN

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

EIN

B

Der C ++ - Compiler sieht den ersten Aufruf von foo () und kann nur zulassen, dass ein normaler Aktivierungsrahmen abgewickelt wird, wenn foo () eine Ausnahme auslöst. Mit anderen Worten, der C ++ - Compiler weiß, dass an dieser Stelle kein zusätzlicher Code erforderlich ist, um den bei der Ausnahmebehandlung auszuführenden Frame-Unwind-Prozess zu unterstützen.

Sobald jedoch ein String s erstellt wurde, weiß der C ++ - Compiler, dass er ordnungsgemäß zerstört werden muss, bevor ein Frame-Unwind zugelassen werden kann, falls später eine Ausnahme auftritt. Der zweite Aufruf von foo () unterscheidet sich also semantisch vom ersten. Wenn der zweite Aufruf von foo () eine Ausnahme auslöst (was er möglicherweise tut oder nicht), muss der Compiler Code platziert haben, der für die Zerstörung von Strings vorgesehen ist, bevor der normale Frame abgewickelt werden kann. Dies unterscheidet sich von dem Code, der für den ersten Aufruf von foo () erforderlich ist.

(Es ist möglich, zusätzliche Dekorationen in C ++ hinzuzufügen , um dieses Problem zu begrenzen. Tatsache ist jedoch, dass Programmierer, die C ++ verwenden, die Auswirkungen jeder von ihnen geschriebenen Codezeile viel besser kennen müssen.)

Im Gegensatz zu Cs malloc signalisiert C ++ 's new mithilfe von Ausnahmen, wenn keine Zuweisung von Rohspeicher möglich ist. So wird 'dynamic_cast'. (Die Standardausnahmen in C ++ finden Sie in Stroustrups 3. Ausgabe, Die C ++ - Programmiersprache, Seite 384 und 385.) Durch Compiler kann dieses Verhalten möglicherweise deaktiviert werden. Im Allgemeinen entstehen Ihnen jedoch aufgrund ordnungsgemäß ausgebildeter Ausnahmebehandlungs-Prologe und -Epiloge im generierten Code zusätzliche Kosten, selbst wenn die Ausnahmen tatsächlich nicht stattfinden und selbst wenn die zu kompilierende Funktion keine Ausnahmebehandlungsblöcke enthält. (Stroustrup hat dies öffentlich beklagt.)

Ohne eine teilweise Template-Spezialisierung (nicht alle C ++ - Compiler unterstützen sie) kann die Verwendung von Templates für die eingebettete Programmierung eine Katastrophe bedeuten. Ohne sie ist Code Bloom ein ernstes Risiko, das ein Embedded-Projekt mit kleinem Arbeitsspeicher blitzschnell zum Erliegen bringen könnte.

Wenn eine C ++ - Funktion ein Objekt zurückgibt, wird ein unbenannter Compiler temporär erstellt und zerstört. Einige C ++ - Compiler können effizienten Code bereitstellen, wenn in der return-Anweisung anstelle eines lokalen Objekts ein Objektkonstruktor verwendet wird, wodurch der Konstruktions- und Zerstörungsaufwand für ein Objekt verringert wird. Aber nicht jeder Compiler tut dies, und viele C ++ - Programmierer sind sich dieser "Rückgabewertoptimierung" nicht einmal bewusst.

Das Bereitstellen eines Objektkonstruktors mit einem einzigen Parametertyp kann es dem C ++ - Compiler ermöglichen, einen Konvertierungspfad zwischen zwei Typen auf völlig unerwartete Weise für den Programmierer zu finden. Diese Art von "klugem" Verhalten ist nicht Teil von C.

Eine catch-Klausel, die einen Basistyp angibt, "schneidet" ein geworfenes abgeleitetes Objekt, da das geworfene Objekt unter Verwendung des "statischen Typs" der catch-Klausel und nicht des "dynamischen Typs" des Objekts kopiert wird. Eine nicht seltene Ursache für Ausnahmefehler (wenn Sie der Meinung sind, dass Sie sich sogar Ausnahmen in Ihrem eingebetteten Code leisten können).

C ++ - Compiler können automatisch Konstruktoren, Destruktoren, Kopierkonstruktoren und Zuweisungsoperatoren mit unbeabsichtigten Ergebnissen für Sie generieren. Es braucht Zeit, um sich mit den Details vertraut zu machen.

Das Übergeben von Arrays abgeleiteter Objekte an eine Funktion, die Arrays von Basisobjekten akzeptiert, generiert selten Compiler-Warnungen, führt jedoch fast immer zu falschem Verhalten.

Da C ++ den Destruktor von teilweise konstruierten Objekten nicht aufruft, wenn eine Ausnahme im Objektkonstruktor auftritt, erfordert die Behandlung von Ausnahmen in Konstruktoren normalerweise "intelligente Zeiger", um sicherzustellen, dass konstruierte Fragmente im Konstruktor ordnungsgemäß zerstört werden, wenn dort eine Ausnahme auftritt . (Siehe Stroustrup, Seite 367 und 368.) Dies ist ein häufiges Problem beim Schreiben guter Klassen in C ++, wird jedoch in C natürlich vermieden, da in C keine Semantik für Konstruktion und Zerstörung eingebaut ist von Unterobjekten innerhalb eines Objekts bedeutet das Schreiben von Code, der mit diesem einzigartigen semantischen Problem in C ++ fertig werden muss; mit anderen Worten "herumschreiben" von semantischen C ++ - Verhaltensweisen.

C ++ kopiert möglicherweise Objekte, die an Objektparameter übergeben werden. In den folgenden Fragmenten wird beispielsweise der Aufruf "rA (x);" kann dazu führen, dass der C ++ - Compiler einen Konstruktor für den Parameter p aufruft, um dann den Kopierkonstruktor aufzurufen, um das Objekt x an den Parameter p zu übertragen kopiert von Parameter p. Schlimmer noch, wenn Klasse A ihre eigenen Objekte hat, die gebaut werden müssen, kann dies katastrophale Folgen haben. (AC-Programmierer würden den größten Teil dieses Mülls vermeiden, da C-Programmierer keine so praktische Syntax haben und alle Details einzeln ausdrücken müssen.)

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

Zum Schluss noch eine kurze Anmerkung für C-Programmierer. longjmp () hat in C ++ kein portierbares Verhalten. (Einige C-Programmierer verwenden dies als eine Art "Ausnahmemechanismus".) Einige C ++ - Compiler werden tatsächlich versuchen, die Dinge so einzurichten, dass sie bereinigt werden, wenn longjmp verwendet wird, aber dieses Verhalten ist in C ++ nicht übertragbar. Wenn der Compiler erstellte Objekte bereinigt, ist er nicht portierbar. Wenn der Compiler sie nicht bereinigt, werden die Objekte nicht zerstört, wenn der Code aufgrund des longjmp den Bereich der erstellten Objekte verlässt und das Verhalten ungültig ist. (Wenn die Verwendung von longjmp in foo () keinen Bereich hinterlässt, ist das Verhalten möglicherweise in Ordnung.) Dies wird von C-Embedded-Programmierern nicht allzu oft verwendet, sie sollten sich jedoch vor der Verwendung über diese Probleme im Klaren sein.


4
Diese Art von Funktionen, die nur einmal verwendet werden, werden niemals als Funktionsaufruf kompiliert, der Code wird einfach ohne Aufruf dort abgelegt.
Dorian

6
@Dorian - Ihr Kommentar kann unter bestimmten Umständen für bestimmte Compiler zutreffen. Wenn die Funktion in der Datei statisch ist, hat der Compiler die Möglichkeit , den Code inline zu machen. Wenn es von außen sichtbar ist, muss es eine Möglichkeit geben, dass die Funktion aufgerufen werden kann, auch wenn es nie tatsächlich aufgerufen wird.
uɐɪ

1
@jonk - Ein weiterer Trick, den Sie in einer guten Antwort nicht erwähnt haben, besteht darin, einfache Makrofunktionen zu schreiben, die die Initialisierung oder Konfiguration als erweiterten Inline-Code ausführen. Dies ist besonders nützlich bei sehr kleinen Prozessoren, bei denen die Tiefe von RAM / Stack / Funktionsaufrufen begrenzt ist.
uɐɪ

@ ʎəʞouʎəʞ Ja, ich habe die Erörterung von Makros in C verpasst. Diese sind in C ++ veraltet, aber eine Erörterung zu diesem Punkt könnte nützlich sein. Ich kann es ansprechen, wenn ich etwas Nützliches finden kann, um darüber zu schreiben.
Jonk

1
@jonk - Ich stimme Ihrem ersten Satz überhaupt nicht zu. Ein Beispiel, wie inline static void turnOnFan(void) { PORTAbits &= ~(1<<8); }es an zahlreichen Stellen genannt wird, ist ein perfekter Kandidat.
Jason S

8

1) Code für Lesbarkeit und Wartbarkeit zuerst. Der wichtigste Aspekt einer Codebasis ist, dass sie gut strukturiert ist. Schön geschriebene Software neigt dazu, weniger Fehler zu haben. Möglicherweise müssen Sie in ein paar Wochen / Monaten / Jahren Änderungen vornehmen, und es ist immens hilfreich, wenn Ihr Code gut lesbar ist. Oder vielleicht muss jemand anderes etwas ändern.

2) Die Leistung von Code, der einmal ausgeführt wird, spielt keine große Rolle. Sorge für Stil, nicht für Leistung

3) Auch Code in engen Schleifen muss in erster Linie korrekt sein. Wenn Sie Leistungsprobleme haben, optimieren Sie, sobald der Code korrekt ist.

4) Wenn Sie optimieren müssen, müssen Sie messen! Es ist egal, ob Sie denken oder ob Ihnen jemand sagt, dass dies static inlinenur eine Empfehlung für den Compiler ist. Sie müssen sich ansehen, was der Compiler macht. Sie müssen auch messen, ob Inlining die Leistung verbessert hat. In eingebetteten Systemen müssen Sie auch die Codegröße messen, da der Codespeicher normalerweise ziemlich begrenzt ist. Dies ist DIE wichtigste Regel, die Technik von Vermutungen unterscheidet. Wenn Sie es nicht gemessen haben, hat es nicht geholfen. Engineering misst. Die Wissenschaft schreibt es auf;)


2
Die einzige Kritik, die ich an Ihrem ansonsten ausgezeichneten Beitrag habe, ist Punkt 2). Die Leistung des Initialisierungscodes ist zwar irrelevant - in einer eingebetteten Umgebung kann die Größe jedoch eine Rolle spielen. (Aber das hat keinen Vorrang Nummer 1; starten Größe zu optimieren , wenn Sie benötigen - und nicht vor)
Martin Bonner unterstützt Monica

2
Die Leistung des Initialisierungscodes ist möglicherweise zunächst irrelevant. Wenn Sie den Energiesparmodus hinzufügen und eine schnelle Wiederherstellung durchführen möchten, um das Aufweckereignis zu verarbeiten, wird dieser relevant.
Berendi - Protest

5

Wenn eine Funktion nur an einer Stelle aufgerufen wird (auch innerhalb einer anderen Funktion), setzt der Compiler den Code immer an diese Stelle, anstatt die Funktion wirklich aufzurufen. Wenn die Funktion an vielen Stellen aufgerufen wird, ist es sinnvoll, eine Funktion zumindest aus Sicht der Codegröße zu verwenden.

Nach dem Kompilieren wird der Code nicht mehr mehrfach aufgerufen, sondern die Lesbarkeit wird stark verbessert.

Außerdem möchten Sie beispielsweise den ADC-Init-Code in derselben Bibliothek mit anderen ADC-Funktionen haben, die nicht in der Haupt-C-Datei enthalten sind.

In vielen Compilern können Sie verschiedene Optimierungsstufen für die Geschwindigkeit oder die Codegröße angeben. Wenn Sie also eine kleine Funktion haben, die an vielen Stellen aufgerufen wird, wird die Funktion "eingebettet" und dort kopiert, anstatt aufzurufen.

Die Geschwindigkeitsoptimierung integriert Funktionen an so vielen Stellen wie möglich, die Codegrößenoptimierung ruft die Funktion jedoch auf, wenn eine Funktion nur an einer Stelle aufgerufen wird, da sie in Ihrem Fall immer "integriert" ist.

Code wie folgt:

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

wird kompilieren zu:

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

ohne irgendeinen Anruf zu benutzen.

Und die Antwort auf Ihre Frage, in Ihrem Beispiel oder ähnlichem, die Lesbarkeit des Codes hat keinen Einfluss auf die Leistung, nichts ist viel in der Geschwindigkeit oder der Codegröße. Es ist üblich, mehrere Aufrufe zu verwenden, um den Code lesbar zu machen. Am Ende werden sie als Inline-Code ausgeführt.

Aktualisieren Sie, um anzugeben, dass die obigen Anweisungen nicht für absichtlich beschädigte kostenlose Versionscompiler wie die kostenlose Version von Microchip XCxx gültig sind. Diese Art von Funktionsaufrufen ist eine Goldgrube für Microchip, um zu zeigen, wie viel besser die kostenpflichtige Version ist. Wenn Sie diese kompilieren, werden Sie in der ASM genau so viele Aufrufe finden, wie Sie im C-Code haben.

Es ist auch nicht für dumme Programmierer, die erwarten, Zeiger auf eine eingebettete Funktion zu verwenden.

Dies ist der Elektronikbereich, nicht der allgemeine C C ++ - oder Programmierbereich. Die Frage betrifft die Mikrocontroller-Programmierung, bei der jeder anständige Compiler standardmäßig die oben genannte Optimierung vornimmt.

Bitte hören Sie nur deshalb mit dem Abstimmen auf, weil dies in seltenen, ungewöhnlichen Fällen möglicherweise nicht zutrifft.


15
Ob Code inline wird oder nicht, ist ein spezifisches Problem bei der Implementierung des Compilerherstellers. Selbst die Verwendung des Inline-Schlüsselworts garantiert keinen Inline-Code. Dies ist ein Hinweis für den Compiler. Mit Sicherheit werden gute Compiler Funktionen, die sie kennen, nur einmal inline verwenden. Dies ist jedoch normalerweise nicht der Fall, wenn sich "flüchtige" Objekte im Bereich befinden.
Peter Smith

9
Diese Antwort ist einfach nicht wahr. Wie @PeterSmith sagt, hat der Compiler gemäß der C-Sprachspezifikation die Möglichkeit , den Code inline zu schreiben, kann dies jedoch nicht und wird dies in vielen Fällen auch nicht tun. Es gibt so viele verschiedene Compiler auf der Welt für so viele verschiedene Zielprozessoren, die in dieser Antwort eine pauschale Aussage treffen und davon ausgehen, dass alle Compiler Code inline platzieren, wenn sie nur die Option haben, nicht haltbar zu sein.
uɐɪ

2
@ ʎəʞouʎəʞ Sie weisen auf seltene Fälle hin, in denen dies nicht möglich ist, und es wäre eine schlechte Idee, eine Funktion überhaupt nicht aufzurufen. Ich habe noch nie einen so dummen Compiler gesehen, der in dem einfachen Beispiel, das das OP gibt, wirklich call verwendet.
Dorian

6
In Fällen, in denen diese Funktionen nur einmal aufgerufen werden, ist die Optimierung des Funktionsaufrufs so gut wie kein Problem. Muss das System während des Setups wirklich jeden einzelnen Taktzyklus rückgängig machen? Wie bei der Optimierung überall: Schreiben Sie lesbaren Code und optimieren Sie ihn nur, wenn die Profilerstellung zeigt, dass er benötigt wird .
Baldrickk

5
@MSalters Es geht mir nicht darum, was der Compiler hier macht - eher darum, wie der Programmierer damit umgeht. Es gibt entweder keinen oder einen zu vernachlässigenden Leistungseinbruch bei der Initialisierung, wie in der Frage zu sehen.
Baldrickk

2

Erstens gibt es kein Bestes oder Schlimmstes; Es ist alles eine Ansichtssache. Sie sind sehr richtig, dass dies ineffizient ist. Es kann optimiert werden oder nicht; es hängt davon ab, ob. Normalerweise sehen Sie diese Arten von Funktionen, Uhr, GPIO, Timer usw. in separaten Dateien / Verzeichnissen. Compiler waren im Allgemeinen nicht in der Lage, über diese Lücken hinweg zu optimieren. Es gibt einen, den ich kenne, der aber für solche Dinge nicht weit verbreitet ist.

Einzelne Datei:

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();
}

Auswählen eines Ziels und eines Compilers zu Demonstrationszwecken.

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

Dies ist, was die meisten Antworten hier sagen, dass Sie naiv sind und dass dies alles optimiert wird und die Funktionen entfernt werden. Nun, sie werden nicht entfernt, da sie standardmäßig global definiert sind. Wir können sie entfernen, wenn sie außerhalb dieser einen Datei nicht benötigt werden.

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();
}

Entfernt sie jetzt, sobald sie eingebettet sind.

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

Aber die Realität ist, wenn Sie Chip-Anbieter oder BSP-Bibliotheken übernehmen,

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

Sie werden definitiv anfangen, Overhead hinzuzufügen, was sich spürbar auf Leistung und Platz auswirkt. Ein paar bis fünf Prozent von jedem, abhängig davon, wie klein jede Funktion ist.

Warum wird das überhaupt gemacht? Einiges davon ist das Regelwerk, das Professoren beibringen würden oder immer noch, um die Benotung von Code zu vereinfachen. Funktionen müssen auf eine Seite passen (zurück, wenn Sie Ihre Arbeit auf Papier ausgedruckt haben), dies nicht tun, das nicht tun, usw. Vieles davon besteht darin, Bibliotheken mit gemeinsamen Namen für verschiedene Ziele zu erstellen. Wenn Sie Dutzende Familien von Mikrocontrollern haben, von denen einige Peripheriegeräte gemeinsam nutzen und andere nicht, können drei oder vier verschiedene UART-Varianten in den Familien, verschiedene GPIOs, SPI-Controller usw. verwendet werden. Sie können eine generische Funktion gpio_init () verwenden. get_timer_count () usw. Diese Abstraktionen können für die verschiedenen Peripheriegeräte wiederverwendet werden.

Es handelt sich hauptsächlich um Wartbarkeit und Software-Design mit einer gewissen Lesbarkeit. Wartbarkeit, Lesbarkeit und Leistung, die Sie nicht alle haben können; Sie können jeweils nur eine oder zwei auswählen, nicht alle drei.

Dies ist in hohem Maße eine meinungsbasierte Frage, und das Obige zeigt die drei wichtigsten Wege, die dies gehen kann. Welcher Weg der BESTE ist, ist ausschließlich eine Meinung. Erledigt man die ganze Arbeit in einer Funktion? Eine meinungsbasierte Frage, einige Leute neigen zu Leistung, andere definieren Modularität und ihre Version der Lesbarkeit als BEST. Das interessante Problem mit dem, was viele Leute als Lesbarkeit bezeichnen, ist äußerst schmerzhaft. um den Code zu "sehen", müssen 50-10.000 Dateien gleichzeitig geöffnet sein und irgendwie versuchen, die Funktionen in der Ausführungsreihenfolge linear zu sehen, um zu sehen, was los ist. Ich finde das Gegenteil von Lesbarkeit, aber andere finden es lesbar, da jedes Element in das Bildschirm- / Editorfenster passt und als Ganzes verwendet werden kann, nachdem sie die aufgerufenen Funktionen gespeichert haben und / oder einen Editor haben, der ein- und ausgeblendet werden kann jede Funktion innerhalb eines Projekts.

Das ist ein weiterer wichtiger Faktor, wenn Sie verschiedene Lösungen sehen. Texteditoren, IDEs usw. sind sehr persönlich und gehen über vi gegen Emacs hinaus. Programmiereffizienz, Zeilen pro Tag / Monat steigen, wenn Sie mit dem verwendeten Tool vertraut und effizient sind. Die Funktionen des Tools können / werden absichtlich oder nicht darauf abzielen, wie die Fans dieses Tools Code schreiben. Wenn eine Person diese Bibliotheken schreibt, spiegelt das Projekt diese Gewohnheiten in gewissem Maße wider. Selbst wenn es sich um ein Team handelt, können die Gewohnheiten / Vorlieben des leitenden Entwicklers oder Chefs dem Rest des Teams auferlegt werden.

Codierungsstandards, in denen eine Menge persönlicher Vorlieben verankert sind, sehr religiöser vi gegen Emacs, Tabulatoren gegen Leerzeichen, wie Klammern aneinandergereiht sind usw. Und diese spielen in gewissem Maße eine Rolle bei der Gestaltung der Bibliotheken.

Wie solltest DU deins schreiben? Wie auch immer Sie wollen, es gibt wirklich keine falsche Antwort, wenn es funktioniert. Es ist sicher, dass es sich um schlechten oder riskanten Code handelt. Wenn er jedoch so geschrieben ist, dass Sie ihn bei Bedarf warten können, erfüllt er Ihre Entwurfsziele, gibt die Lesbarkeit und einige Wartungsmöglichkeiten auf, wenn Leistung wichtig ist, oder umgekehrt. Mögen Sie kurze Variablennamen, damit eine einzelne Codezeile in die Breite des Editorfensters passt? Oder lange, zu beschreibende Namen, um Verwirrung zu vermeiden, aber die Lesbarkeit nimmt ab, weil Sie nicht eine Zeile auf einer Seite erhalten können. Jetzt ist es visuell aufgebrochen und verwirrt den Fluss.

Sie werden nicht das erste Mal einen Homerun schlagen. Es kann / sollte Jahrzehnte dauern, um Ihren Stil wirklich zu definieren. Gleichzeitig kann sich in dieser Zeit Ihr Stil ändern, indem Sie sich für eine Weile in die eine und dann in die andere Richtung neigen.

Sie werden hören, dass viele nicht optimieren, nie optimieren und vorzeitig optimieren. Aber wie gezeigt, verursachen solche Designs von Anfang an Leistungsprobleme. Dann werden Hacks angezeigt, mit denen das Problem gelöst werden kann, anstatt von Anfang an neu zu designen, um eine Leistung zu erbringen. Ich bin damit einverstanden, dass es Situationen gibt, eine einzelne Funktion, ein paar Codezeilen, die Sie versuchen können, den Compiler zu manipulieren, aus Angst vor dem, was der Compiler sonst tun wird. Wenn Sie während des Schreibens optimieren und wissen, wie der Compiler den Code kompilieren wird, möchten Sie zunächst überprüfen, wo sich der Cycle Stealer wirklich befindet, bevor Sie ihn angreifen.

Sie müssen in gewissem Umfang auch Ihren Code für den Benutzer entwerfen. Wenn dies Ihr Projekt ist, sind Sie der einzige Entwickler. es ist was immer du willst. Wenn Sie versuchen, eine Bibliothek zum Verschenken oder Verkaufen zu machen, möchten Sie wahrscheinlich, dass Ihr Code wie alle anderen Bibliotheken aussieht, Hunderte bis Tausende von Dateien mit winzigen Funktionen, langen Funktionsnamen und langen Variablennamen. Trotz der Probleme mit der Lesbarkeit und der Leistung werden Sie feststellen, dass mehr Leute diesen Code verwenden können.


4
"Ja wirklich?" Was für "irgendein Ziel" und "irgendein Compiler" verwendest du, darf ich fragen?
Dorian

Es sieht für mich eher nach einem 32/64-Bit-ARM8 aus, vielleicht von einem Himbeer-PI als von einem üblichen Mikrocontroller. Hast du den ersten Satz in der Frage gelesen?
Dorian

Nun, der Compiler entfernt nicht verwendete globale Funktionen nicht, der Linker jedoch. Wenn es richtig konfiguriert und verwendet wird, werden sie nicht in der ausführbaren Datei angezeigt.
Berendi - Protest

Wenn sich jemand fragt, welcher Compiler über Dateilücken hinweg optimieren kann: Die IAR-Compiler unterstützen die Kompilierung mehrerer Dateien (so nennen sie es), was eine dateiübergreifende Optimierung ermöglicht. Wenn Sie alle c / cpp-Dateien auf einmal darauf werfen, erhalten Sie eine ausführbare Datei, die eine einzige Funktion enthält: main. Die Leistungsvorteile können sehr tiefgreifend sein.
Arsenal

3
@Arsenal Natürlich unterstützt gcc Inlining, auch über Kompilierungseinheiten hinweg, wenn es richtig aufgerufen wird. Siehe gcc.gnu.org/onlinedocs/gcc/Optimize-Options.html und suchen Sie nach der Option -flto.
Peter - Reinstate Monica

1

Sehr allgemeine Regel - der Compiler kann besser optimieren als Sie. Natürlich gibt es Ausnahmen, wenn Sie sehr schleifenintensive Aufgaben ausführen, aber insgesamt sollten Sie Ihren Compiler mit Bedacht auswählen, wenn Sie eine gute Optimierung für Geschwindigkeit oder Codegröße wünschen.


Leider ist es für die meisten Programmierer heute wahr.
Dorian

0

Es hängt sicher von Ihrem eigenen Codierungsstil ab. Eine allgemeine Regel, die es gibt, ist, dass Variablennamen sowie Funktionsnamen so klar und selbsterklärend wie möglich sein sollten. Je mehr Unteraufrufe oder Codezeilen Sie in eine Funktion einfügen, desto schwieriger wird es, eine eindeutige Aufgabe für diese eine Funktion zu definieren. In Ihrem Beispiel haben Sie eine Funktion initModule(), die Dinge initialisiert und Unterprogramme aufruft, die dann die Uhr stellen oder die Konfiguration einstellen . Sie können das erkennen, indem Sie nur den Namen der Funktion lesen. Wenn Sie den gesamten Code aus den Unterprogrammen initModule()direkt in Ihr Programm einfügen , wird weniger offensichtlich, was die Funktion tatsächlich tut. Aber wie so oft ist es nur eine Richtlinie.


Danke für Ihre Antwort. Ich kann den Stil ändern, wenn dies für die Leistung erforderlich ist, aber die Frage ist, ob die Lesbarkeit des Codes die Leistung beeinträchtigt.
MaNyYaCk

Ein Funktionsaufruf führt zu einem Aufruf oder einem Befehl jmp, aber das ist meiner Meinung nach ein vernachlässigbarer Verlust an Ressourcen. Wenn Sie Entwurfsmuster verwenden, werden Sie manchmal mit einem Dutzend Funktionsaufrufen konfrontiert, bevor Sie den eigentlichen Code erreichen.
po.pe

@Humpawumpa - Wenn Sie für einen Mikrocontroller mit nur 256 oder 64 Byte RAM schreiben, ist ein Dutzend Funktionsaufrufe kein vernachlässigbares Opfer, es ist einfach nicht möglich
8:23 Uhr

Ja, aber das sind zwei Extreme ... normalerweise haben Sie mehr als 256 Bytes und verwenden weniger als ein Dutzend Schichten - hoffentlich.
po.pe

0

Wenn eine Funktion wirklich nur eine sehr kleine Aufgabe erfüllt, sollten Sie sie in Betracht ziehen static inline.

Fügen Sie es einer Header-Datei anstelle der C-Datei hinzu und static inlinedefinieren Sie es mit den Worten :

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

Wenn die Funktion jetzt noch etwas länger ist, z. B. über 3 Zeilen, ist es möglicherweise eine gute Idee, sie zu vermeiden static inlineund der C-Datei hinzuzufügen. Schließlich haben eingebettete Systeme nur begrenzten Arbeitsspeicher, und Sie möchten die Codegröße nicht zu stark erhöhen.

Wenn Sie die Funktion in definieren file1.cund von dort aus verwenden, wird sie vom file2.cCompiler nicht automatisch eingebunden. Wenn Sie es jedoch file1.hals static inlineFunktion definieren, wird es wahrscheinlich von Ihrem Compiler eingebunden.

Diese static inlineFunktionen sind bei der Hochleistungsprogrammierung äußerst nützlich. Ich habe festgestellt, dass sie die Code-Leistung oft um den Faktor drei steigern.


"wie über 3 Zeilen sein" - Zeilenzählung hat nichts damit zu tun; inlining cost hat alles damit zu tun. Ich könnte eine 20-Zeilen-Funktion schreiben, die perfekt zum Inlinen ist, und eine 3-Zeilen-Funktion, die zum Inlinen schrecklich ist (z. B. functionA (), die functionB () dreimal aufruft, functionB (), die functionC () dreimal aufruft, und ein paar andere Ebenen).
Jason S

Wenn Sie die Funktion in definieren file1.cund von dort aus verwenden, wird sie vom file2.cCompiler nicht automatisch eingebunden. Falsch . Siehe zB -fltoin gcc oder clang.
Berendi - protestiert

0

Eine Schwierigkeit beim Versuch, effizienten und zuverlässigen Code für Mikrocontroller zu schreiben, besteht darin, dass einige Compiler bestimmte Semantiken nur dann zuverlässig verarbeiten können, wenn Code compilerspezifische Anweisungen verwendet oder viele Optimierungen deaktiviert.

Wenn Sie beispielsweise ein Single-Core-System mit einer Interrupt-Serviceroutine haben [ausgeführt von einem Timer-Tick oder was auch immer]:

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++)
  }
}

Es sollte möglich sein, Funktionen zu schreiben, um eine Hintergrundschreiboperation zu starten, oder auf den Abschluss zu warten:

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;
}

und dann solchen Code aufrufen mit:

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.

Leider entscheidet ein "kluger" Compiler wie gcc oder clang, dass die ersten Schreibvorgänge bei aktivierter vollständiger Optimierung keinen Einfluss auf die Beobachtbarkeit des Programms haben und daher optimiert werden können. Qualitativ hochwertige Compiler iccsind dafür weniger anfällig, wenn das Setzen eines Interrupts und das Warten auf den Abschluss sowohl flüchtige Schreibvorgänge als auch flüchtige Lesevorgänge (wie hier der Fall) umfassen, aber die Plattform, die von angestrebt wirdicc ist für eingebettete Systeme nicht so beliebt.

Der Standard ignoriert bewusst Implementierungsqualitätsprobleme und geht davon aus, dass es mehrere sinnvolle Möglichkeiten gibt, mit dem obigen Konstrukt umzugehen:

  1. Eine Qualitätsimplementierung, die ausschließlich für Felder wie das Knacken von High-End-Zahlen gedacht ist, kann vernünftigerweise erwarten, dass für solche Felder geschriebener Code keine Konstrukte wie die oben genannten enthält.

  2. Eine Qualitätsimplementierung kann alle Zugriffe auf volatileObjekte so behandeln, als würden sie Aktionen auslösen, die auf Objekte zugreifen, die für die Außenwelt sichtbar sind.

  3. Eine einfache Implementierung mit angemessener Qualität, die für die Verwendung in eingebetteten Systemen vorgesehen ist, kann alle Aufrufe von Funktionen, die nicht als "Inline" gekennzeichnet sind, so behandeln, als ob sie auf Objekte zugreifen könnten, die der Außenwelt ausgesetzt waren, auch wenn sie nicht volatilewie in # beschrieben behandelt werden. 2.

Der Standard unternimmt keinen Versuch vorzuschlagen, welcher der oben genannten Ansätze für eine Qualitätsimplementierung am besten geeignet wäre, und verlangt nicht, dass "konforme" Implementierungen von ausreichend guter Qualität sind, um für einen bestimmten Zweck verwendet werden zu können. Folglich erfordern einige Compiler wie gcc oder clang effektiv, dass jeder Code, der dieses Muster verwenden möchte, mit vielen deaktivierten Optimierungen kompiliert werden muss.

In einigen Fällen kann es ein vernünftiges Minimum sein, sicherzustellen, dass sich die E / A-Funktionen in einer separaten Kompilierungseinheit befinden und ein Compiler keine andere Wahl hat, als anzunehmen, dass er auf eine beliebige Teilmenge von Objekten zugreifen kann, die der Außenwelt ausgesetzt waren. Eine böse Art, Code zu schreiben, der zuverlässig mit gcc und clang funktioniert. In solchen Fällen besteht das Ziel jedoch nicht darin, die zusätzlichen Kosten eines unnötigen Funktionsaufrufs zu vermeiden, sondern die unnötigen Kosten im Gegenzug zu akzeptieren, um die erforderliche Semantik zu erhalten.


"Sicherstellen, dass sich die E / A-Funktionen in einer separaten Kompilierungseinheit befinden" ... ist keine sichere Methode, um solche Optimierungsprobleme zu vermeiden. Zumindest LLVM und ich sind der Meinung, dass GCC in vielen Fällen eine Ganzprogrammoptimierung durchführen wird. Sie können sich daher entscheiden, Ihre E / A-Funktionen zu integrieren, selbst wenn sie sich in einer separaten Kompilierungseinheit befinden.
Jules

@Jules: Nicht alle Implementierungen eignen sich zum Schreiben eingebetteter Software. Das Deaktivieren der Ganzprogrammoptimierung ist möglicherweise die kostengünstigste Methode, um gcc oder clang zu zwingen, sich als eine für diesen Zweck geeignete Qualitätsimplementierung zu verhalten.
Supercat

@Jules: Eine Implementierung mit höherer Qualität für die Embedded- oder Systemprogrammierung sollte so konfigurierbar sein, dass sie eine für diesen Zweck geeignete Semantik aufweist, ohne die Ganzprogrammoptimierung vollständig deaktivieren zu müssen (z. B. durch die Option, volatileZugriffe so zu behandeln, als ob sie möglicherweise ausgelöst würden) willkürliche Zugriffe auf andere Objekte), aber aus welchen Gründen auch immer würden gcc und clang Implementierungsqualitätsprobleme eher als Aufforderung behandeln, sich nutzlos zu verhalten.
Supercat

1
Selbst die Implementierungen von "höchster Qualität" korrigieren den Buggy-Code nicht. Wenn dies buffnicht deklariert ist volatile, wird es nicht als flüchtige Variable behandelt. Die Zugriffe darauf können neu angeordnet oder vollständig optimiert werden, wenn sie anscheinend nicht später verwendet werden. Die Regel ist einfach: Markieren Sie alle Variablen, auf die außerhalb des normalen Programmflusses (vom Compiler aus gesehen) zugegriffen werden kann, als volatile. Werden die Inhalte von buffin einem Interrupt-Handler zugegriffen? Ja. Dann sollte es sein volatile.
Berendi - Protest

@berendi: Compiler können Garantien anbieten, die über die Anforderungen des Standards hinausgehen, und Qualitäts-Compiler werden dies tun. Eine hochwertige freistehende Implementierung für eingebettete Systeme ermöglicht es Programmierern, Mutex-Konstrukte zu synthetisieren, was im Wesentlichen dem Code entspricht. Wenn magic_write_countNull ist, gehört der Speicher der Hauptleitung. Wenn es nicht Null ist, gehört es dem Interrupt-Handler. Herstellung von buffflüchtigen erfordern würde , dass jede Funktion überall, die darauf verwenden arbeitet , volatileQualifizieren Zeiger, die Optimierung weit mehr als mit einem Compiler beeinträchtigen würde ...
supercat
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.