floatadcval=adc_read_avg(v_solar);// between 0.0 to 1.0
2
intvcc=2500/adc_read_avg(v_ref);// mV
3
Timert;
4
5
t.reset();t.start();
6
temp=adcval*vcc*1056/56;
7
t.stop();
8
serial.printf("1: voltage = %f mV, duration = %d us\n",temp,t.read_us());
9
10
t.reset();t.start();
11
temp=adcval*1056*vcc/56;
12
t.stop();
13
serial.printf("2: voltage = %f mV, duration = %d us\n",temp,t.read_us());
14
15
t.reset();t.start();
16
temp=adcval*1056/56*vcc;
17
t.stop();
18
serial.printf("3: voltage = %f mV, duration = %d us\n",temp,t.read_us());
19
20
t.reset();t.start();
21
temp=adcval*105.6/5.6*vcc;
22
t.stop();
23
serial.printf("4: voltage = %f mV, duration = %d us\n",temp,t.read_us());
produziert bei mir folgende Ausgabe:
1
1: voltage = 12866.679688 mV, duration = 6 us
2
2: voltage = 12866.678711 mV, duration = 10 us
3
3: voltage = 12866.678711 mV, duration = 22 us
4
4: voltage = 12866.679688 mV, duration = 7 us
5
6
1: voltage = 12889.782227 mV, duration = 7 us
7
2: voltage = 12889.783203 mV, duration = 10 us
8
3: voltage = 12889.783203 mV, duration = 22 us
9
4: voltage = 12889.783203 mV, duration = 6 us
10
11
1: voltage = 12835.620117 mV, duration = 7 us
12
2: voltage = 12835.619141 mV, duration = 10 us
13
3: voltage = 12835.620117 mV, duration = 22 us
14
4: voltage = 12835.620117 mV, duration = 7 us
Kann mir jemand erklären, warum die Umstellung der Konstanten für den
Spannungsteiler (100k und 5.6k) so große Unterschiede in der Laufzeit
verursacht?
Ich vermute, dass es mit impliziter Konvertierung zwischen float und int
zu tun hat?
Intuitiv hätte ich gedacht, dass 1 und 2 entweder unterschiedliche
Genauigkeit haben (da 1056 / 56 schon zusammengefasst und gerundet
werden) oder die Rechenzeit gleich ist (vcc * 1056) und (1056 * vcc) ist
beides mal int * int.
Ich hätte auch vermutet, dass Variante 4 am längsten dauert
(insbesondere länger als Variante 3), da explizit mit float gerechnet
wird...
Dass die Genauigkeitsunterschiede ein bisschen zufällig aussehen kann
ich mir auch nicht ganz erklären.
Toolchain: g++ (GNU Tools for ARM Embedded Processors) 4.8.4 20140725
(release) [ARM/embedded-4_8-branch revision 213147]
Martin
Du solltest alle Variabeln einheitlich zu floats casten. Damit nimmst du
das dem Programm zur Laufzeit ab. Das kann zu unterschiedlichen
Laufzeiten führen.
Klammern setzen würde auch nicht schaden, wäre mehr human readable, hat
aber keinen Einfluss auf die Laufzeit. ;)
Zu berücksichtigen ist auch, dass andere Faktoren geben kann, warum die
Rechenoperatoren länger brauchen. Z.B. andere Programme oder
Hintergrundskripte.
Martin J. schrieb:> Kann mir jemand erklären, warum die Umstellung der Konstanten für den> Spannungsteiler (100k und 5.6k) so große Unterschiede in der Laufzeit> verursacht?
Weil von links nach rechts gerechnet wird.
> Ich vermute, dass es mit impliziter Konvertierung zwischen float und int> zu tun hat?
Muss ja so sein. Deshalb solltest du die Umwandlung besser explizit
casten. Aber ich würde mir hier einfach mal das erzeugte Assemblerfile
anschauen...
H. E. schrieb:> Zu berücksichtigen ist auch, dass andere Faktoren geben kann, warum die> Rechenoperatoren länger brauchen. Z.B. andere Programme oder> Hintergrundskripte.
Ich vermute sehr, dass es sich hier um einen uC (siehe Forum) handelt,
auf dem genau 1 Programm läuft.
Ich hätte eigentlich erwartet, dass Variante 3 die schnellste ist,
weil hier auf ein Zwischenergebnis (adcval * 1056) von Variante 2
zurückgegriffen werden kann und somit eine Rechenoperation einspart
wird. Interessanterweise ist aber Variante 3 sogar die langsamste.
Falls es sich um einen Prozessor mit Cache oder Branch-Prediction
handelt: Diese beiden Features führen oft dazu, dass langsamer
aussehender Code schneller ausgeführt wird und umgekehrt.
Ich vermute, bei Variante 1 wird "1056 / 56" einfach vom Kompiler
gerechnet und die Division fällt damit komplett raus.
Das Gleiche bei Variante 3 "105.6 / 5.6"
Welche Optimierung -O hattest du an?
Lothar M. schrieb:>> Ich vermute, dass es mit impliziter Konvertierung zwischen float und int>> zu tun hat?> Muss ja so sein. Deshalb solltest du die Umwandlung besser explizit> casten. Aber ich würde mir hier einfach mal das erzeugte Assemblerfile> anschauen...
Ich auch...
Möglich wäre außer float/int-Konvertierung auch Unterschiede in der
signed/unsigned-Optimierung oder -Typcasting, denn was den beiden
schnellen Varianten gleich ist, ist dass sie zuerst float * signed int
rechnen, dann float * (const) unsigned int (im ersten Fall, im vierten
Fall gar nicht).
Denkbar wäre auch, dass die Rechnungen teilweise auf Zwischenergebnisse
der vorherigen Rechnungen zurückgreifen - ich weiß nicht, wie gut der
Optimierer ist...
Aber wie Lothar schon sagt: Assemblerfile ansehen dürfte viele der
Fragen beantworten.
MfG, Arno
Ist ein STM32F072.
Und ja, habe darauf geachtet, dass nicht noch irgendwelche Interrupts
aktiv sind, die zwischendurch noch "im Hintergrund" ausgeführt werden
könnten.
Zugegebenermaßen komme ich eher aus der C++ als Assembler-Ecke. Aber
sehe mir den objdump gerade mal an und werde den relevanten Part gleich
mal posten.
Änder mal die Reihenfolge. Also zuerst die langsamsten, dann die
schnellsten, ob sich da was ändert. Nicht das das was mit der
Quantisierung des Timers zu tun hat. Oder am besten jeweils eine
Schleife rum und 1 Million mal ausführen.
Dumdi D. schrieb:> Änder mal die Reihenfolge. Also zuerst die langsamsten, dann die> schnellsten, ob sich da was ändert. Nicht das das was mit der> Quantisierung des Timers zu tun hat. Oder am besten jeweils eine> Schleife rum und 1 Million mal ausführen.
Ok, hier mal zwei unterschiedliche Reihenfolgen.
Letztes nach vorne gezogen ergibt quasi keinen Unterschied:
1
4: voltage = 12888.063477 mV, duration = 6 us
2
1: voltage = 12888.063477 mV, duration = 8 us
3
2: voltage = 12888.063477 mV, duration = 10 us
4
3: voltage = 12888.063477 mV, duration = 21 us
Langsamstes zuerst führt tatsächlich zu einer Änderung:
1
3: voltage = 12958.458984 mV, duration = 27 us
2
2: voltage = 12958.459961 mV, duration = 7 us
3
1: voltage = 12958.459961 mV, duration = 6 us
4
4: voltage = 12958.458984 mV, duration = 6 us
Jetzt werde ich mal versuchen, aus dem ASM schlau zu werden...
Martin J. schrieb:> Jetzt werde ich mal versuchen, aus dem ASM schlau zu werden...
Ich habe den Eindruck, da wird kräftig Code hin und her verschoben, so
dass du vermutlich mit keinem der Timer-Start-Stop-Aufrufe wirklich
genau eine Rechnung misst. Atmel beschreibt den Effekt für avr-gcc ganz
gut hier:
http://www.atmel.com/webdoc/avrlibcreferencemanual/optimization_1optim_code_reorder.html
(mit Unterstützung von PeDa)
Wenn ich es richtig sehe, ist BL ein "unconditional branch", also ein
"Funktionsaufruf", und folgende Funktionen tauchen auf:
__aeabi_i2f dürfte integer zu float umwandeln
__aeabi_fmul dürfte zwei floats multiplizieren
__aeabi_fdiv zwei floats dividieren und
__aeabi_f2d dürfte einen float in einen double umwandeln
add und adds sind 32Bit-Additionen, adds setzt die diversen Flags
(Carry, Negative, Zero...) und add nicht.
Aber vermutlich hast du das auch alles schon herausgefunden und
versuchst gerade, die einzelnen Variablen nachzuverfolgen :)
MfG, Arno
-Konstanten so anlegen, dass sie gleich vom Compiler berechnet werden
und nicht jedes Mal zur Laufzeit
-Inline benutzen, bringt einiges an Geschwindigkeit, Funktionen
aufzurufen kostet Zeit
-Hilfsvariablen müssen nicht schlimm sein, sprich nichts doppelt
rechnen, gerade auf einem uC der zum Teil mehrere Zyklen für eine
Floatoperation benötigt
-Deine Werte könnten ggf. auch mit Ganzzahlen gerechnet werden, brauchst
du wirklich float?
Wenn Du nicht die höchste Code-Optimierung eingeschaltet hast, ist ein
Vergleich sinnlos, weil der Compiler es Dir zum Debuggen eventuell
angenehm machen will und nicht die Rechenleistung zählt.
Wenn man mit Optimierung übersetzt, dann kann man nicht sicher sein,
dass die Operationen exakt in der gezeigten Reihenfolge durchgeführt
werden, und genau so wie hingeschrieben.
So kommt es hier vor, dass nur ein Teil der Rechnung im
Start/Stop-Intervall des Timers liegt, der Rest der Rechnung dahinter in
der Parameterversorgung von printf liegt.
Erster Teil des Codes:
Start Timer
1x int->float
Stop Timer
Read Timer
2x Multiplikation
1x Division
1x float->double
printf
Hier wird also nur die Dauer der i->f Konvertierung gemessen, vmtl. von
vcc. Der Rest der Rechnung erfolgt erst nach dem Stop vom Timer.
In den anderen Teilen dahinter fehlt wiederum die i->f Konvertierung,
denn die hat ja bereits stattgefunden und muss nicht nochmal
durchgeführt werden.
NB: Besonders peinlich sind solche Effekte in Fällen wie hier:
t = a / b;
DisableInt();
volatile_var = t;
EnableInt();
Da kann es passieren, dass die langsame Division genau da stattfindet,
wo man sie nicht haben will, nämlich im Bereich mit abgeschalteten
Interrupts.
Vielleicht wäre es geschickt, für jede Berechnungsart eine Funktion zu
verwenden, die immer alle notwendigen Schritte ausführen muß und nicht
auf vorherige Zwischenergebnisse zurückgreifen kann.
Wie auch immer, trotz "böser" float-Rechnerei sind die Berechnungen so
schnell, daß man nicht gelangweilt mit einer Stoppuhr in der Hand
daneben stehen kann.
m.n. schrieb:> Vielleicht wäre es geschickt, für jede Berechnungsart eine Funktion zu> verwenden, die immer alle notwendigen Schritte ausführen muß und nicht> auf vorherige Zwischenergebnisse zurückgreifen kann.
Und sicherstellen, dass diese Funktionen nicht inlined werden.
A. K. schrieb:> Und sicherstellen, dass diese Funktionen nicht inlined werden.
Das genügt heutzutage allerdings nicht mehr, d.h. attribute((noinline))
reicht nicht aus.
Johann L. schrieb:> Das genügt heutzutage allerdings nicht mehr, d.h. attribute((noinline))> reicht nicht aus.
Es sollte aber wohl reichen, die Berechnungsfunktionen in ein separates
Objektfile auszulagern. Wenn man dann normal übersetzt, also nicht die
all-in-one Übersetzung verwendet.
Arno schrieb:> ich weiß nicht, wie gut der Optimierer ist...
Der ist exzellent.
Was einem hier auf die Füße fällt ist nicht der Optimierer, sondern es
sind die Restriktionen der Sprache. Es ist nämlich
1
a/b*c
etwas anderes als
1
a*c/b
und daher ist das, was der TO macht, zwar "Umgestellten von
Gleichungen", aber eben in Gleichungen -- gemein sind Ausdrücke -- die
nicht gleichbedeutend zueinander sind. Und das trifft auch dann zu,
wenn alle Konstanten brav nach float gecastet werden.
Falls die Stringenz in der float-Behandlung nicht erforderlich ist,
hilft z.B. -ffast-math oder ähnliche Optionen. Siehe GCC-Doku.
Hab gerade mal die Berechnung in eine separate Funktion gepackt und für
jede Variante neu compiliert und geflasht. Ergebnis: Alle brauchen 6 us
(also zumindest laut Timer-Aufrufen außerhalb der Funktion selbst).
Ich glaube ich werde mich einfach damit abfinden, dass man nicht genau
vorhersagen kann, wie der Compiler die Befehle schiebt.
Danke auf jeden Fall für die guten Antworten!