Bei solchen Performance-Tests solltest du drauf achten, dass das
Beispiel wirklich sehr gut dem später produktiv verwendeten Code
entspricht. Also beispielsweise dass es sich durchweg um globale
Variablen handelt und der Zugriff auf die Variablen exakt einmal
erfolgt.
Sonst kann es nämlich leicht passieren, dass das Ergebnis dir nicht
wirklich weiter hilft. Nicht selten geschieht es auch, dass Testcode vom
Compiler als überflüssiger Unfug erkannt und zusammengestrichen wird.
Beispiel: Direkte Adressierung ist dann schneller, wenn sie ein einziges
Mal erfolgt. Wird auf die gleiche Variable zigmal zugegriffen kann die
indirekte Variante besser sein. Vorausgesetzt natürlich, die ungenannte
Zielmaschine hat überhaupt eine nennenswerte Anzahl Register. Und
verschiedene Compiler können zu sehr verschiedenen Erkenntnissen führen.
Und noch was: Nicht jede CPU unterstützt die willkürliche Mischung
verschiedene Adressierungsarten. Es kommt also auch auf die
Vorgeschichte. Welcher Wert kommt wo her? Konstanten, Register, mem ...
Dann ist ein etwas komplexerer uC wohl auch soetwas wie eine Blackbox?
Wenn es auf die Vorgeschichte, etc. ankommt, wie gewährleistet man dann
den effektivsten (also schnellsten) Code geschrieben zu haben?
Gibt es nicht soetwas wie eine "Richtlinie"?
*Hansi schrieb:> Dann ist ein etwas komplexerer uC wohl auch soetwas wie eine Blackbox?> Wenn es auf die Vorgeschichte, etc. ankommt, wie gewährleistet man dann> den effektivsten (also schnellsten) Code geschrieben zu haben?
Durch Rückkopplung. C-Code schreiben, erzeugten Code inspizieren, C-Code
verändern, ...
> Gibt es nicht soetwas wie eine "Richtlinie"?
Entsteht durch obigen Prozess. Es hilft allerdings auch, wenn man dank
Erfahrung mit Compilern und Zielmaschinen eine Vorstellung von deren
Denkweise und der Umsetzung von C-Code in Maschinencode hat.
Es ist etwas mehr als nur eine Richtlinie und abhängig sowohl vom
Compiler als auch von der Zielarchitektur, manchmal auch vom
tatsächlichen Zielprozessor (also beispielsweise Pentium III vs 4).
Gerade Anfänger legen meiner Meinung nach viel zu viel Augenmerk auf
low-level Optimierungen.
Compilerbauer sind nicht doof. Die haben ihrem Compiler
Optimierungsstrategien mitgegeben, mit denen er schon sehr viel aus dem
Code herausholen kann.
Als Neuling sollte man sich im ersten Anlauf darauf beschränken, die
jeweils sinnvollen kleinstmöglichen Datentypen auszuwählen. Damit
erreicht man schon eine ganze Menge. Und für den Rest gilt: In erster
Linie zeichnet sich guter Code dahingehend aus, dass er sauber und
lesbar ist.
Mit der Zeit wird man dann sowieso besser. Man verwendet bessere
Verfahren, bessere und schnellere Algorithmen, hat eine Menge
algorithmischer Tricks auf Lager, die bei Bedarf eingesetzt werden. Erst
dann ist der Zeitpunkt gekommen, an dem man dann tatsächlich anfängt im
Assembleroutput des Compilers nachzusehen, ob man noch den einen oder
anderen Taktzyklus einsparen kann. Aber erst dann, wenn zuvor alles
andere ausgeschöpft wurde.
Auch ein solides Grundverständnis, wie die Sprache C funktioniert bringt
allemal mehr, als so mancher 'Tip' in diesem Optimierungslink.
Karl Heinz Buchegger schrieb:> Erst> dann ist der Zeitpunkt gekommen, an dem man dann tatsächlich anfängt im> Assembleroutput des Compilers nachzusehen, ob man noch den einen oder> anderen Taktzyklus einsparen kann
Tja, und der ist wohl gekommen.
Habe für ein Projekt alles "quick & dirty" programmiert, globale/lokale
Variablen immer direkt verändert/übergeben, usw.
Das Programm hat wunderbar funktioniert, die Ausführzeiten wahren sehr
gut.
Irgendwann wollte ich dann mal den Code ein bisschen säubern, habe es
teilweis umgebaut und Pointer verwendet.
Und nun könnte ich gerade *kotz...* die Ausführzeit hat sich verdoppelt.
Ich habe damals an der Uni gelernt (also noch zu Zeiten von uC/51), dass
man bei Controllern generell viel mit Pointern arbeiten soll.
Nun im Jahr 2011 angekommen, mein erster 32-Bitter, scheint das wohl
nicht mehr so wirklich der Fall zu sein.
Wenn man bereits beim ersten Versuch über einen solchen Fallstrick
stolpert, kann es nur besser werden.
Vielen Dank für eure Mühen,
Hans Peter
*Hansi schrieb:> Irgendwann wollte ich dann mal den Code ein bisschen säubern, habe es> teilweis umgebaut und Pointer verwendet.> Und nun könnte ich gerade *kotz...* die Ausführzeit hat sich verdoppelt.
Logisch.
Wenn du immer nur einen Zettel vorfindest, auf dem steht "Die
Information steht im Regal 5, 3. Zettel von links", dann brauchst du
doppelt so lange. Erst mal musst du den ersten Zettel lesen und dann mit
dieser Information den anderen Zettel auffinden, auf dem dann die
tatsächliche Information steht. Du hast also 2 Zugriffe, wo es einer
auch tut, wenn du von vorne herein schon weißt, dass du im Regal 5
nachsehen musst. Das weiß aber der Compiler/Linker, denn die haben ja
die Variablen im Speicher angeordnet.
> Ich habe damals an der Uni gelernt (also noch zu Zeiten von uC/51), dass> man bei Controllern generell viel mit Pointern arbeiten soll.
Unsinn.
> Nun im Jahr 2011 angekommen, mein erster 32-Bitter, scheint das wohl> nicht mehr so wirklich der Fall zu sein.
Dieser "Rat" war auch damals schon Unsinn.
Man verwendet Pointer, wenn man sie verwenden muss. Und wenn es nicht
sein muss, dann lässt man es.
*Hansi schrieb:> Nun im Jahr 2011 angekommen, mein erster 32-Bitter, scheint das wohl> nicht mehr so wirklich der Fall zu sein.
An dieser Stelle unterscheiden sich 32-Bitter ziemlich deutlich von
vielen 8-Bittern.
Daumenregel: Bei 32-Bittern ist der Zugriff auf globale Variablen viel
teurer als der Zugriff auf lokale Variablen. Globale Variablen sollten
daher nur verwendet werden wo wirklich nötig. Soweit möglich lokale
Variablen verwenden. Parameterübergabe ist oft effizienter als die
Verwendung globaler Variablen.
Thematisch zusamenhängende globale Variablen können effizienter sein,
wenn sie zu einer struct zusammengefasst werden. Je nach Intelligenz des
Compilers und Häufigkeit des Zugriffs kann es sich dann auch lohnen,
die Adresse dieser struct anfangs in einer Funktion in einen Pointer zu
laden und über diesen Pointer zuzugreifen.
Ein globaler Pointer als "Optimierung" für Zugriff auf die immer gleiche
bekannte Variable ist wohl immer Unfug.
Also sind call-by-reference-Aufrufe von Funktionen auch insgesamt nicht
immer schneller als call-by-value?
Denn bei Übergabe der Adresse einer Variablen als Argument muss zwar
deren Wert nicht kopiert werden, dafür dauern aber die Zugriffe per
indirekter Adressierung in der Funktion länger?
Johannes F. schrieb:> Also sind call-by-reference-Aufrufe von Funktionen auch insgesamt nicht> immer schneller als call-by-value?
Das hängt vom Datentyp des Arguments ab.
> Denn bei Übergabe der Adresse einer Variablen als Argument muss zwar> deren Wert nicht kopiert werden, dafür dauern aber die Zugriffe per> indirekter Adressierung in der Funktion länger?
Genau.
Wenn daher die Kopie schneller erstellt ist, als die indirekten Zugriffe
dauern, dann hast du pessimiert.
Edit: Ausnahme natürlich, wenn die Funktion in der Lage sein soll, das
Argument beim Aufrufer zu verändern. Dann hast du sowieso keine andere
Wahl und musst Pointer benutzen. Aber darum gehts ja nicht (denke ich
zumindest)
Johannes F. schrieb:> Denn bei Übergabe der Adresse einer Variablen als Argument muss zwar> deren Wert nicht kopiert werden, dafür dauern aber die Zugriffe per> indirekter Adressierung in der Funktion länger?
Zugriffe über Adressparameter sind meist effizienter als direkte
Zugriffe auf globale Variablen. Allerdings kann es noch besser sein, den
Wert der Variablen direkt zu übergeben und den neuen Wert als Returnwert
zurück zu bekommen, wenn in der Funktion öfter darauf zugegriffen wird.
A. K. schrieb:> Daumenregel: Bei 32-Bittern ist der Zugriff auf globale Variablen viel> teurer als der Zugriff auf lokale Variablen. Globale Variablen sollten> daher nur verwendet werden wo wirklich nötig. Soweit möglich lokale> Variablen verwenden. Parameterübergabe ist oft effizienter als die> Verwendung globaler Variablen.
Das würde ich so nicht kaufen. Die Adresse einer globalen Variable kann
schon der Linker einsetzen, die lokale Adresse muß immer zur Laufzeit
relativ zum Framepointer berechnet werden.
A. K. schrieb:> Ein globaler Pointer als "Optimierung" für Zugriff auf die immer gleiche> bekannte Variable ist wohl immer Unfug.
Da bin ich bei dir, indirekt ist eben indirekt.
MfG Klaus
Klaus schrieb:> Das würde ich so nicht kaufen.
Solltest du aber.
> Die Adresse einer globalen Variable kann> schon der Linker einsetzen, die lokale Adresse muß immer zur Laufzeit> relativ zum Framepointer berechnet werden.
Ich beziehe mich ausdrücklich auf 32-Bitter. Zugriffe relativ zum Stack-
oder Framepointer sind dort stets einfache Standardbefehle. Die
Adressrechnung kostet nichts extra sondern ist Teil der Pipeline.
Zugriffe auf globale Daten hingegen müssen bei RISCs (ARM/Cortex, AVR32,
PIC32, ...) aus mehreren Befehlen zusammengesetzt werden und sind auch
bei CISCs (Coldfire, x86) vom Code her länger als die Register-relative
Adressierung.
Ausserdem landen lokale Variablen und Parameter bei 32-Bittern meist in
Registern, nicht auf dem Stack. Zumindest wenn es nicht zu viele sind
und sie keine Adresse haben müssen. Und Register sind die mit Abstand
effizienteste Speicherung.
A. K. schrieb:> Ich beziehe mich ausdrücklich auf 32-Bitter. Zugriffe relativ zum Stack-> oder Framepointer sind dort stets einfache Standardbefehle. Zugriffe auf> globale Daten hingegen müssen bei RISCs (ARM/Cortex, AVR32, PIC32, ...)> aus mehreren Befehlen zusammengesetzt werden und sind auch bei CISCs> (Coldfire, x86) länger codiert.
Bei (den großen) RISCs stimme ich dir zu, hättest du aber gleich sagen
können. Bei den kleinen kann das aber anders sein.
Auf meinem Atom (x86) gibt es keinen signifikanten Laufzeitunterschied
zwischen der Verwendung von lokalen oder globalen Variablen.
MfG Klaus
Klaus schrieb:> Bei (den großen) RISCs stimme ich dir zu, hättest du aber gleich sagen> können. Bei den kleinen kann das aber anders sein.
Dass ich 32-Bitter meinte stand doch direkt im Text drin. Welche
kleineren 32-Bitter hast du hier im Auge? Viel kleiner als ARM gibts
fast nicht. Allenfalls Zilogs irre 32-Bit Z80.
> Auf meinem Atom (x86) gibt es keinen signifikanten Laufzeitunterschied> zwischen der Verwendung von lokalen oder globalen Variablen.
Ach je! Ich bringe als Beispiele fast ausschliesslich typische 32-Bit
Mikrocontroller (ARM, Cortex, AVR32, PIC32, Coldfire), die du
seltsamerweise "grosse RISCs" nennst. Und du kommst mit dem sehr sehr
viel grösseren Intel Atom. Was soll das denn?
NB: Ja, beim Atom ist relative vs. absolute Adressierung meist neutral,
oder fast. Trotzdem ist die relative Adressierung meist 3 Bytes kürzer
als die absolute und (mindestens) genauso schnell.
x86 hat ein bischen wenig Register, was entsprechende Optimierung
erschwert. Je nach ABI gehören die ausserdem zu den wenigen 32-Bittern,
die Parameter konsequent per Stack übergeben. Bei AMD64 (64-Bit x86)
sieht das schon ganz anders aus.
A. K. schrieb:> Dass ich 32-Bitter meinte stand doch direkt im Text drin
Ok, ich merke mir 32 Bit heißt RISC bei dir.
A. K. schrieb:> die du> seltsamerweise "grosse RISCs" nennst
Die AVRs kenne ich nicht so genau, aber PIC kleiner als 32 würde ich als
kleine RISCs bezeichnen.
Und der Atom ist der kleinste gängige x86 heutzutage.
MfG Klaus
Klaus schrieb:> Ok, ich merke mir 32 Bit heißt RISC bei dir.
Ich hatte sowohl Coldfire als auch x86 ausdrücklich erwähnt, beides
CISC. Für beide gilt diese Regel ebenfalls, wenngleich mitunter in
geringerem Ausmass. Auch da sind Register effizienter als Speicher und
auch da macht kleinerer Code irgendwann einen Unterschied.
Es gibt übrigens noch weitere Aspekte: Lokale Variablen sind auch sonst
besser optimierbar, weil der Compiler meist weiss, dass niemand sonst
darauf zugreift. Globale Variablen lassen sich über Aufrufe von
Funktionen hinweg meist nicht optimieren, weil der Compiler nicht weiss,
ob die Variablen darin verwendet werden. Dazu kommen noch mögliche
Aliasing-Risiken, die auch fast nur globale Variablen betreffen (bei
Skalaren).
> Die AVRs kenne ich nicht so genau, aber PIC kleiner als 32 würde ich als> kleine RISCs bezeichnen.
Aber die würde ich nicht als 32-Bitter bezeichnen. Und ich hatte meine
Aussage ausdrücklich auf 32-Bitter bezogen.
Darüber hinaus hatte ich es als Daumenregel bezeichnet, nicht als
ehernes Gesetz. Allerdings würde ich gern mal ein realistisches Beispiel
sehen, wo es mit globalen Variablen deutlich besser funktioniert als mit
lokalen.
> Und der Atom ist der kleinste gängige x86 heutzutage.
Liegt aber immer noch Welten oberhalb der Mikrocontroller. Für das was
hier besprochen wird ist der Unterschied zwischen Atom und Sandy Bridge
wenig relevant.
A. K. schrieb:>> Die AVRs kenne ich nicht so genau, aber PIC kleiner als 32 würde ich als>> kleine RISCs bezeichnen.>> Aber die würde ich nicht als 32-Bitter bezeichnen. Und ich hatte meine> Aussage ausdrücklich auf 32-Bitter bezogen.
War auch nur die Antwort auf deine Frage, was ich als "nicht" große
RISCs bezeichnen würde.
Aber etwas zurück zum Thema: Variablen sollten da angelegt werden, wo
sie nach der Logik des Programms hingehören, und Pointer sollten benutzt
werden, wenn die Programmlogik einen Pointer erfordert. Alles andere
rächt sich, wenn man in einem Jahr was am Programm ändern muß. Ansonsten
sollte man eine schnellere CPU nehmen.
MfG Klaus
Klaus schrieb:> Aber etwas zurück zum Thema: Variablen sollten da angelegt werden, wo> sie nach der Logik des Programms hingehören, und Pointer sollten benutzt> werden, wenn die Programmlogik einen Pointer erfordert.
D'accord.
Ich hatte das angesprochen, weil manche Leute von 8-Bittern her gewohnt
sind, dass lokale und globale Variablen kaum einen Unterschied machen.
Manchmal sogar globale/statische Variablen besser sind als lokale: z.B.
beim PIC18 mit nicht erweiterten Befehlssatz und lokalen Daten auf dem
Stack, wie das der C18 in Standardeinstellung realisiert - aber damit
nicht wirklich gut zurecht kommt.
Da kann es dann passieren, das gewohnheitsmässig sehr viele Daten global
liegen, ob von der Programmlogik her sinnvoll oder nicht. Wenn man dann
auf beispielsweise Cortex-M3 wechselt, dann kann sich dieser Stil als
ungünstig erweisen.
>> Ich habe damals an der Uni gelernt (also noch zu Zeiten von uC/51), dass>> man bei Controllern generell viel mit Pointern arbeiten soll.>Unsinn.
Er meinte wohl kleinere Controller, die nicht so viel ProgramSpeicher
mit sich rum schleppen können.
>Zugriffe auf globale Daten hingegen müssen bei RISCs (ARM/Cortex, AVR32,>PIC32, ...) aus mehreren Befehlen zusammengesetzt werden
Nicht, wenn bereits ein Register auf eine glob. Tabelle zeigt
>Und noch was: Nicht jede CPU unterstützt die willkürliche Mischung>verschiedene Adressierungsarten.
willkürliche Mischung?
entweder die CPU kann die Adressierungsart, oder sie kann es nicht.
*Hansi schrieb:> Ich habe damals an der Uni gelernt (also noch zu Zeiten von uC/51), dass> man bei Controllern generell viel mit Pointern arbeiten soll.
Das musst Du falsch in Erinnerung haben. Diese pauschale Aussage ist
schlichtweg falsch... und sie war schon immer falsch - nicht nur damals.
Die Verwendung eines Pointers kann eine Geschwindigkeitssteigerung beim
Zugriff auf Array-Elemente zur Folge haben, insbesondere dann, wenn das
Array mehrdimensional ist. Denn dann muss die CPU u.U. einige
Multiplikationen durchführen, um auf das Element des Arrays a[i][j][k]
zuzugreifen. Willst Du da linear auf nebeneinanderliegende Elemente
(z.B. in einer Schleife) zugreifen, bist Du mit einem Pointer besser
bedient. Den brauchst Du nur zu inkrementieren. Aber auch das kriegen
gute C-Compiler meistens auch selbst hin, d.h. sie verwenden insgeheim
Pointer, obwohl Du mit Array-Indices arbeitest.