Forum: Mikrocontroller und Digitale Elektronik Merkwürdige Laufzeitunterschiede bei umgestellten Gleichungen (float + int)


von Martin J. (martin-j)


Lesenswert?

Moin zusammen,

folgender Code:
1
float adcval = adc_read_avg(v_solar);  // between 0.0 to 1.0
2
int vcc = 2500 / adc_read_avg(v_ref);  // mV
3
Timer t;
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

von H. E. (hobby_elektroniker)


Lesenswert?

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.

: Bearbeitet durch User
von Lothar M. (Firma: Titel) (lkmiller) (Moderator) Benutzerseite


Lesenswert?

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.

: Bearbeitet durch Moderator
von Yalu X. (yalu) (Moderator)


Lesenswert?

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.

von Mampf F. (mampf) Benutzerseite


Lesenswert?

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?

: Bearbeitet durch User
von Arno (Gast)


Lesenswert?

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

von Martin J. (martin-j)


Lesenswert?

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.

von Dumdi D. (dumdidum)


Lesenswert?

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

von Martin J. (martin-j)


Lesenswert?

Hier schon mal der Assembler Output. Habe die Stellen zwischen Timer 
start und stop mit Leerzeichen abgesetzt.
1
 8002cc0:  f7ff fbc5   bl  800244e <_ZN4mbed5Timer5startEv>
2
 8002cc4:  9802        ldr  r0, [sp, #8]
3
 8002cc6:  f000 fc47   bl  8003558 <__aeabi_i2f>
4
 8002cca:  1c06        adds  r6, r0, #0
5
 8002ccc:  a80c        add  r0, sp, #48  ; 0x30
6
 8002cce:  f7ff fbdf   bl  8002490 <_ZN4mbed5Timer4stopEv>
7
8
 8002cd2:  a80c        add  r0, sp, #48  ; 0x30
9
 8002cd4:  f7ff fbeb   bl  80024ae <_ZN4mbed5Timer7read_usEv>
10
 8002cd8:  1c31        adds  r1, r6, #0
11
 8002cda:  1c07        adds  r7, r0, #0
12
 8002cdc:  1c28        adds  r0, r5, #0
13
 8002cde:  f000 faf1   bl  80032c4 <__aeabi_fmul>
14
 8002ce2:  495b        ldr  r1, [pc, #364]  ; (8002e50 <main+0x200>)
15
 8002ce4:  f000 faee   bl  80032c4 <__aeabi_fmul>
16
 8002ce8:  495a        ldr  r1, [pc, #360]  ; (8002e54 <main+0x204>)
17
 8002cea:  f000 f9c5   bl  8003078 <__aeabi_fdiv>
18
 8002cee:  f001 fe6d   bl  80049cc <__aeabi_f2d>
19
 8002cf2:  4c53        ldr  r4, [pc, #332]  ; (8002e40 <main+0x1f0>)
20
 8002cf4:  1c02        adds  r2, r0, #0
21
 8002cf6:  34bc        adds  r4, #188  ; 0xbc
22
 8002cf8:  1c0b        adds  r3, r1, #0
23
 8002cfa:  9700        str  r7, [sp, #0]
24
 8002cfc:  4956        ldr  r1, [pc, #344]  ; (8002e58 <main+0x208>)
25
 8002cfe:  1c20        adds  r0, r4, #0
26
 8002d00:  f7ff fb8a   bl  8002418 <_ZN4mbed6Stream6printfEPKcz>
27
 8002d04:  a80c        add  r0, sp, #48  ; 0x30
28
 8002d06:  f7ff fbdf   bl  80024c8 <_ZN4mbed5Timer5resetEv>
29
 8002d0a:  a80c        add  r0, sp, #48  ; 0x30
30
 
31
 8002d0c:  f7ff fb9f   bl  800244e <_ZN4mbed5Timer5startEv>
32
 8002d10:  494f        ldr  r1, [pc, #316]  ; (8002e50 <main+0x200>)
33
 8002d12:  1c28        adds  r0, r5, #0
34
 8002d14:  f000 fad6   bl  80032c4 <__aeabi_fmul>
35
 8002d18:  1c07        adds  r7, r0, #0
36
 8002d1a:  a80c        add  r0, sp, #48  ; 0x30
37
 8002d1c:  f7ff fbb8   bl  8002490 <_ZN4mbed5Timer4stopEv>
38
 
39
 8002d20:  a80c        add  r0, sp, #48  ; 0x30
40
 8002d22:  f7ff fbc4   bl  80024ae <_ZN4mbed5Timer7read_usEv>
41
 8002d26:  1c31        adds  r1, r6, #0
42
 8002d28:  9003        str  r0, [sp, #12]
43
 8002d2a:  1c38        adds  r0, r7, #0
44
 8002d2c:  f000 faca   bl  80032c4 <__aeabi_fmul>
45
 8002d30:  4948        ldr  r1, [pc, #288]  ; (8002e54 <main+0x204>)
46
 8002d32:  f000 f9a1   bl  8003078 <__aeabi_fdiv>
47
 8002d36:  f001 fe49   bl  80049cc <__aeabi_f2d>
48
 8002d3a:  1c0b        adds  r3, r1, #0
49
 8002d3c:  9903        ldr  r1, [sp, #12]
50
 8002d3e:  1c02        adds  r2, r0, #0
51
 8002d40:  9100        str  r1, [sp, #0]
52
 8002d42:  1c20        adds  r0, r4, #0
53
 8002d44:  4945        ldr  r1, [pc, #276]  ; (8002e5c <main+0x20c>)
54
 8002d46:  f7ff fb67   bl  8002418 <_ZN4mbed6Stream6printfEPKcz>
55
 8002d4a:  a80c        add  r0, sp, #48  ; 0x30
56
 8002d4c:  f7ff fbbc   bl  80024c8 <_ZN4mbed5Timer5resetEv>
57
 8002d50:  a80c        add  r0, sp, #48  ; 0x30
58
 
59
 8002d52:  f7ff fb7c   bl  800244e <_ZN4mbed5Timer5startEv>
60
 8002d56:  1c38        adds  r0, r7, #0
61
 8002d58:  493e        ldr  r1, [pc, #248]  ; (8002e54 <main+0x204>)
62
 8002d5a:  f000 f98d   bl  8003078 <__aeabi_fdiv>
63
 8002d5e:  1c31        adds  r1, r6, #0
64
 8002d60:  f000 fab0   bl  80032c4 <__aeabi_fmul>
65
 8002d64:  1c06        adds  r6, r0, #0
66
 8002d66:  a80c        add  r0, sp, #48  ; 0x30
67
 8002d68:  f7ff fb92   bl  8002490 <_ZN4mbed5Timer4stopEv>
68
 
69
 8002d6c:  a80c        add  r0, sp, #48  ; 0x30
70
 8002d6e:  f7ff fb9e   bl  80024ae <_ZN4mbed5Timer7read_usEv>
71
 8002d72:  1c07        adds  r7, r0, #0
72
 8002d74:  1c30        adds  r0, r6, #0
73
 8002d76:  f001 fe29   bl  80049cc <__aeabi_f2d>
74
 8002d7a:  9700        str  r7, [sp, #0]
75
 8002d7c:  1c02        adds  r2, r0, #0
76
 8002d7e:  1c0b        adds  r3, r1, #0
77
 8002d80:  1c20        adds  r0, r4, #0
78
 8002d82:  4937        ldr  r1, [pc, #220]  ; (8002e60 <main+0x210>)
79
 8002d84:  f7ff fb48   bl  8002418 <_ZN4mbed6Stream6printfEPKcz>
80
 8002d88:  a80c        add  r0, sp, #48  ; 0x30
81
 8002d8a:  f7ff fb9d   bl  80024c8 <_ZN4mbed5Timer5resetEv>
82
 8002d8e:  a80c        add  r0, sp, #48  ; 0x30
83
 
84
 8002d90:  f7ff fb5d   bl  800244e <_ZN4mbed5Timer5startEv>
85
 8002d94:  a80c        add  r0, sp, #48  ; 0x30
86
 8002d96:  f7ff fb7b   bl  8002490 <_ZN4mbed5Timer4stopEv>

von Martin J. (martin-j)


Lesenswert?

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

von Arno (Gast)


Lesenswert?

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

von Karl (Gast)


Lesenswert?

-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?

von A. S. (Gast)


Lesenswert?

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.

von (prx) A. K. (prx)


Lesenswert?

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.

: Bearbeitet durch User
von m.n. (Gast)


Lesenswert?

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.

von (prx) A. K. (prx)


Lesenswert?

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.

von Johann L. (gjlayde) Benutzerseite


Lesenswert?

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.

von (prx) A. K. (prx)


Lesenswert?

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.

von Johann L. (gjlayde) Benutzerseite


Lesenswert?

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.

von Martin J. (martin-j)


Lesenswert?

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!

Bitte melde dich an um einen Beitrag zu schreiben. Anmeldung ist kostenlos und dauert nur eine Minute.
Bestehender Account
Schon ein Account bei Google/GoogleMail? Keine Anmeldung erforderlich!
Mit Google-Account einloggen
Noch kein Account? Hier anmelden.