Häufig verwendete Funktionen, Leistungsverbesserungen durch statische Variablen?

Hat das Definieren von Variablen (große Arrays) als statisch innerhalb einer Funktion Leistungsverbesserungen, wenn die Funktion wiederholt aufgerufen werden soll, sagen wir in der Größenordnung von Sekunden?

Sie haben diese "Baugruppe" markiert, sprechen aber von "statisch". Meintest du stattdessen in C?
Dies kann von den Adressierungsmodi des Prozessors abhängen. Es ist möglich, dass etwas, das Stapelzeiger-relative Adressierung in Hardware ausführen kann, mit automatischen Variablen schneller ist. Etwas, das dies nicht kann oder mit guter Unterstützung für absolute Adressierung funktioniert, kann mit statischen besser funktionieren. Die Tatsache, dass Sie ein Array verwenden, verkompliziert die Dinge weiter, da Sie die Basisadresse finden und dann auf einen Offset davon zugreifen müssen. Aber denken Sie daran, dass statische Variablen immer Platz beanspruchen, nicht nur wenn sie verwendet werden.
@IgnacioVazquez-Abrams – es ist wirklich beides. Der konzeptionelle Begriff, der die Frage umrahmt, stammt von C, aber das Implementierungsdetail, von dem es vollständig abhängt, ist Maschinensprache, die normalerweise in Assembler-Mnemonik (und nicht in numerischen Opcodes) für die menschliche Bequemlichkeit ausgedrückt wird.
Eines der nützlichsten Dinge, die Sie tun könnten, wäre, beide Optionen zu kompilieren und dann das Ergebnis zu zerlegen, um es zu untersuchen.
Bin ich etwas zu eifrig, oder sollte diese Frage in programrs.stackexchange oder einen Stackoverflow verschoben werden? Es ist eine interessante Frage, nur nicht für dieses ee.se. Außerdem gibt es zu wenig Informationen, ich denke, wir müssen wissen, was der Kontext ist (z. B. AMD64-Server vs. 8-Bit-Embedded) oder die CPU (Amd64, Pic16, ...). IMHO ist die Frage ohne Kenntnis der CPU oder des Kontexts zu vage, um mit viel mehr als Vermutungen beantwortet zu werden.
Es wäre wahrscheinlich am besten, sich die technischen Datenblätter für Ihre spezielle CPU zu besorgen. Sie sagen Ihnen im Allgemeinen, wie viele Taktzyklen jeder Befehl in jedem verfügbaren Adressierungsmodus benötigt.

Antworten (3)

Kurzum: Es kommt darauf an. Auf mehr Dinge, die Sie wissen möchten.

Einige Architekturen haben Anweisungen und/oder Adressierungsmodi, die auf stapelbezogene Daten zugreifen. Nahezu alle Architekturen haben Befehle und/oder Adressierungsmodi, die auf Daten an einer absoluten Adresse zugreifen. Was schneller ist, hängt von der jeweiligen Architektur ab.

Beachten Sie, dass einige Architekturen nur einen sehr kleinen Offset oder eine (Nullseiten-)Adresse in einer Anweisung zulassen, sodass bei einer solchen Architektur viel davon abhängt, ob der Offset passt. Bei vielen statischen (=globale Lebensdauer) Variablen wird man eher eine kleine Zero-Page-Adresse überschreiten (weil sie sich denselben absoluten Adressraum teilen) als bei vielen automatischen (lokale Lebensdauer, Stack-Offset adressiert). ) Variablen (die jeweils ihren eigenen kleinen SP-Offset haben).

Aber auf modernen Architekturen der PC-Klasse ist der Speicherzugriff (einschließlich Cache-Probleme) oft viel wichtiger als die Befehlsausführung, insbesondere wenn der Code eine enge Schleife ist und die Daten überall verstreut sind. In einem solchen Fall kann die Ausführungsgeschwindigkeit um 10 % variieren, je nachdem, wie genau die Daten in Bezug auf verschiedene Cache-Probleme (Zeilen, Seiten, Puffer usw.) ausgerichtet sind, was von fast allem außer der Mondphase beeinflusst wird, einschließlich andere Anwendungen, die geladen sind oder sogar geladen wurden. Das macht Benchmarking in einer solchen Situation sehr fragwürdig.

Verbesserungen im Vergleich zu was? Ich vermute, Sie haben ein C-Programm (oder Sie verwenden die C-Terminologie, um Ihr Programm zu beschreiben), und Sie möchten es vergleichen

  • (a) großes Array, das in jedem Gültigkeitsbereich als "const" deklariert ist
  • (b) großes Array, das innerhalb einer Funktion als statisch deklariert ist (Funktionsbereich)
  • (c) Großes Array, das außerhalb einer Funktion als statisch deklariert wird (Bereich "Datei")
  • (d) Großes Array, das außerhalb einer Funktion deklariert wird (globaler Gültigkeitsbereich)
  • (e) großes Array innerhalb einer Funktion ("automatisch", Funktionsumfang) ohne Initialisierer
  • (f) großes Array innerhalb einer Funktion ("automatisch", Funktionsbereich) mit einem Initialisierer

(Oder gibt es etwas anderes, mit dem Sie vergleichen möchten?)

automatisch mit Initialisierer vs. andere

Der größte Leistungsunterschied besteht zwischen (f) und den anderen – (f) kopiert den Initialisierer in das Array – und löscht alle verbleibenden Elemente – jedes Mal, wenn Sie die Funktion aufrufen. Die anderen verwenden, was sich gerade im RAM befindet.

Wenn die Funktion normalerweise nur wenige Elemente des Arrays verwendet, Sie aber ein sehr großes Array benötigen, um Sonderfälle zu verarbeiten, wird (f) bei jedem Aufruf viel Zeit damit verschwenden, in jedes Byte des Arrays zu schreiben. im Vergleich zu den anderen Ansätzen, die diese Initialisierung überspringen.

Andererseits muss manchmal eine Funktion ein modifizierbares Array haben, das bei jedem Aufruf auf eine bestimmte Weise zurückgesetzt wird. Wenn Sie (f) wählen, verwendet Ihr Compiler wahrscheinlich den effizientesten Weg, dies zu tun - es ist unwahrscheinlich, dass eine der anderen Methoden (gefolgt von "manuellem" Zurücksetzen des Arrays) schneller ist.

automatisch vs. andere (stack-relativ vs. absolut)

Einige Compiler für einige Prozessoren verwenden für jeden Array-Typ (a) bis (f) genau dieselbe generische Array-Zugriffsanweisung. Bei diesen Compilern hat der bestimmte Array-Typ, den Sie auswählen, keine Auswirkung auf die Laufzeit.

Häufig zwingen Prozessoren einen Compiler, unterschiedliche Anweisungen zu verwenden, um auf unterschiedliche Typen von Arrays zuzugreifen.

Bei Prozessoren mit Cache sind die Unterschiede zwischen diesen Anweisungen im Allgemeinen unbedeutend.

Die meisten Prozessoren haben jedoch keinen Cache, wie z. B. die 8-Bit-Prozessoren, die alle 64-Bit-Prozessoren zusammen übertreffen. In diesen Prozessoren können die Unterschiede in den vom Compiler ausgewählten Anweisungen zu einem merklichen Unterschied in der Laufzeit in einer engen inneren Schleife führen, die ohne viele andere Berechnungen auf Elemente im Array zugreift.

(Mein Eindruck ist, dass "automatische" stapelbezogene Anweisungen auf ARM-Mikrocontrollern am schnellsten sind, während "absolute" Anweisungen für globale und "Datei-Statik" und "Funktions-Statik" auf PIC16-Mikrocontrollern am schnellsten sind).

Im Allgemeinen ist es schneller, auf Variablen zuzugreifen, die zur Erstellungszeit bekannte Adressen haben. Der genaue Nutzen, falls vorhanden, hängt jedoch von vielen anderen Faktoren ab. Welches PIC zum Beispiel? Statisch im Gegensatz zu welcher Art dynamischer Zuordnung? Wie groß ist das Array? Wenn Sie einen Compiler verwenden, kann dies von Strategien abhängen, die vom Compiler-Designer ausgewählt wurden.

Wenn Sie statische (Adresse zum Zeitpunkt des Erstellens bekannt) Variablen mit dynamisch zuordnenden Variablen auf dem Datenstapel vergleichen, dann ist statisch ein Gewinn auf einem einfachen PIC 16, da diese keine inhärenten Stapelfunktionen haben.

Auf einem PIC 18 gibt es einige begrenzte relative Adressierungsfähigkeiten von FSRs, aber diese werden im Allgemeinen immer noch mehr Zyklen benötigen, als wenn eine feste Adresse bekannt ist. Der Offset von einem Zeigerregister beträgt nur 8 Bit, sodass die Reichweite ebenfalls begrenzt ist. Darüber hinaus müssen Sie selbst rechnen, um die Adresse zu berechnen und in einen der FSRs zu laden.

Auf einem 16-Bit-PIC gibt es mehr registerbezogene Adressierungsmodi, die einen flexibleren Zugriff auf Elemente auf dem Stapel ermöglichen. Es stehen jedoch noch mehr Adressierungsmodi zur Verfügung, wenn Sie nicht durch ein Register gehen müssen, um die Basisadresse zu erhalten. In einigen begrenzten Fällen kann es tatsächlich schneller sein, auf eine 16-Bit-Variable auf dem Stack zuzugreifen, da die Adresse des Stacks bereits in W15 ist, während die statische Adresse einer Variablen möglicherweise zuerst in ein Register geladen werden muss. Die Strafe für den Zugriff auf dynamischen Speicher ist bei einem 16-Bit-PIC geringer, aber ich denke, dass der Zugriff auf statischen Speicher insgesamt schneller ist. Es gibt auch einen Abschnitt des Datenspeichers, der als "near" bezeichnet wird und spezielle Anweisungen ermöglicht, die für den Zugriff auf den Rest des Datenspeichers nicht verfügbar sind. In einigen Fällen könnten Sie dafür sorgen, dass sich der Stapel in der Nähe des Speichers befindet, aber das würde nicht

All dies berücksichtigte jedoch einzelne Variablen. Sie fragen nach einem großen Array, was die Dinge wieder anders macht. Auf Elemente eines Arrays wird sowieso indirekt mit der zur Laufzeit berechneten Adresse zugegriffen, sodass es insgesamt weniger einen Unterschied macht, ob die Startadresse des Arrays eine Konstante ist oder etwas, das abgerufen oder berechnet werden muss. Bei einem PIC 18 ist das größte Problem wahrscheinlich die Konkurrenz um die begrenzte Anzahl von FSRs (Hardware-Zeigerregistern), da es nur 3 davon gibt und höchstwahrscheinlich mindestens 1 bereits als Datenstapelzeiger zugewiesen ist. (Der C18-Compiler ist "weniger als brillant" darin, wie er die PIC 18-Zeiger verwendet, und kommandiert tatsächlich 2 der 3 SFRs für seinen eigenen Gebrauch.) Auf einem 16-Bit-PIC werden die Laufzeitzyklen noch weniger unterschiedlich sein.

Ordnen Sie die Dinge also grundsätzlich statisch zu, wenn Sie können. Der einzige Grund, PICs nicht zu verwenden, ist, wenn Sie Speicher sparen müssen und wissen, dass erhebliche Mengen an dynamisch zugewiesenem Speicher niemals gleichzeitig zugewiesen werden. Wenn zum Beispiel zwei Subroutinen jeweils ein 2-kB-Array nur zur vorübergehenden Verwendung verwenden müssen, diese Subroutinen jedoch nie voneinander aufgerufen werden, dann spart die dynamische Zuweisung der Arrays bei Bedarf insgesamt 2 kB im Vergleich zur statischen Zuweisung.

Ein weiterer Grund, insbesondere bei einem PIC 16 und PIC 18, besteht darin, zuzulassen, dass spezielle Schnellzugriffsspeicherplätze als Kratzer innerhalb jeder Routine verwendet werden. Beispielsweise greifen bei vielen PIC 16 die letzten 16 Bytes jeder Bank global auf die gleichen 16 Bytes zu. Diese begrenzten Speicherplätze sind daher als schneller temporärer Scratch sehr nützlich, da sie unabhängig von der Bankeinstellung aufgerufen werden können. Meine Konvention besteht darin, die meisten davon so zu reservieren, als ob sie Register in anderen Prozessorarchitekturen wären. Meine Routinen speichern daher im Allgemeinen diejenigen, die sie beim Eintritt auf dem Datenstapel löschen, und stellen sie beim Verlassen wieder her. Die Zugriffsbank auf einem PIC 18 funktioniert ähnlich, obwohl sie größer ist und daher auch für andere Dinge einfacher zu verwenden ist. Dieses Konzept gilt nicht wirklich für die 16-Bit-PICs, da sie 16 echte Hardware-Register eingebaut haben.

Um es noch einmal zusammenzufassen, weisen Sie alle persistenten Zustände statisch zu. Weisen Sie dann den verbleibenden Status statisch zu, es sei denn, Sie müssen die Speichernutzung reduzieren.