Frage zu selbstgeschriebener Funktion - Laufzeit gegenüber Compiler-Variante

Gast #2805638
Lesenswert?

Hi Leute!

Ich habe rein aus Interesse mal ausprobiert, in wie weit sich Codegröße 
und Laufzeit gegeneinander verhalten, wenn man Divisionen und 
Multiplikationen anstatt mit den üblichen Operatoren "/" und "*" rein 
durch Addition und Subtraktion durchführt. Ich habe mir dazu selber eine 
Funktion geschrieben, die sowohl teilt, als auch multipliziert:
1
int32_t divide_multiply_number( uint8_t operand,
2
                                int32_t source_number,
3
                                int32_t div_multi )
4
{
5
  int32_t div_multi_number = 0;
6
  uint8_t sign = 0;
7

8
  if( (source_number == 0) || (div_multi == 0) )
9
  {
10
    return 0;
11
  }
12

13
  if( source_number < 0 )
14
  {
15
    sign |= 0x01;
16
    source_number = ( source_number - (source_number + source_number));
17
  }
18

19
  if( div_multi < 0 )
20
  {
21
    sign |= 0x02;
22
    div_multi = (div_multi - (div_multi + div_multi));
23
  }
24

25
  if( operand == MULTIPLY )
26
  {
27
    while( div_multi )
28
    {
29
      div_multi_number += source_number;
30
      div_multi--;
31
    }
32
  }
33
  else if( operand == DIVIDE )
34
  {
35
    for( div_multi_number = 0;
36
         source_number >= div_multi;
37
         source_number -= div_multi )
38
    {
39
      div_multi_number++;
40
    }
41
  }
42

43
  if( (sign == 1) || (sign == 2) )
44
  {
45
    return (div_multi_number - (div_multi_number + div_multi_number));
46
  }
47

48
  return div_multi_number;
49
}

Klar, die Funktion sieht erstmal ziemlich übertrieben aus, ist sie wohl 
auch, aber sie arbeitet halt rein durch Addition/Subtraktion.

Im Programm führe ich nun folgende Rechenoperation aus:
1
divide_multiply_number( DIVIDE, (divide_multiply_number( MULTIPLY, adc_value - adc_control.adc_lower_value, 10000 )), adc_control.adc_span )

Die Funktion ruft sich also selber nochmal auf.

So...wenn ich die Boardmittel vom Compiler benutze, also regulär mit 
Punkt und Strich rechne, dann sieht das ganze so aus:
1
(((adc_value - adc_control.adc_lower_value)*10000) / adc_control.adc_span))

Das Fazit des ganzen ist, dass meine eigene Variante nicht nur mehr 
Speicher benötigt (ca. 100Byte), RAM bleibt gleich, sondern auch noch 
extreeeemst viel langsamer ist.

Ich habe dazu einen Pin vor und nach der Rechnung seinen Zustand ändern 
lassen...das Ergebnis: meine eigene Variante braucht wahnsinnige 26ms 
(8MHz Takt), wogegen die normale Variante gerade mal 96us braucht.

Und jetzt die Frage: Wie kommt das? Also was macht der Compiler anders, 
bzw. um wieviel tausend Lichtjahre ist seine Variante besser als die 
eigene? Wie rechnet der uC das intern? Dachte ja eigentlich auch, dass 
Addition/Subtraktion platzsparender ist...aber nichtmal das ;-)

Nun, schlauer bin ich auf jeden Fall, dennoch würde ich gerne mal eure 
Meinungen dazu hören.

Gruß
Gast #2805654
Lesenswert?

hast du wenigsten die Optimierung eingeschaltet?

das Problem könnte auch sein das du eine funktion für beides hast. Wenn 
du es in 2 funktionen zerlegst, dann müsste es schneller sein weil die 
vergleiche wegfallen.
#2805658
Lesenswert?

MisterX schrieb:

> (((adc_value - adc_control.adc_lower_value)*10000) /

> Und jetzt die Frage: Wie kommt das?

Die Compilerfunktionen arbeiten sich auf Bitebene durch die Zahl durch.
Eine 32 Bit Multiplikation ist im Prinzip 32 mal eine (32-Bit)Addition 
mit entweder 0(*) oder der Originalzahl und ein wenig SChieben.

Deine Version hingegen macht konkret 10000 (in Worten: Zehntausend) 
Additionen. Die Version kann nur verlieren. 10000 Addition gegen 32 
Additionen und ein wenig 32 Bit Schieben .... so lang kann das Drumherum 
in einer normalen bitweisen Multiplikation gar nicht dauern

(und wenn der µC multiplizeren in Hardware kann, dann siehts noch 
schlimmer aus)


Fazit: Bei wirklich kleinen Faktoren kann Addition/Subtraktion schneller 
sein. Aber irgendwann ist der Breakeven erreicht, an dem bessere 
Verfahren schneller sind als "Brutale Gewalt", selbst wenn die Brutale 
Gewalt ein wenig einfacher ist.

(*) die natürlich nicht gemacht wird. Nicht das wir uns da falsch 
verstehen.
Gast #2805673
Lesenswert?

MisterX schrieb:
> Und jetzt die Frage: Wie kommt das? Also was macht der Compiler anders,

Nix, nur der Schreiber der internen Funktion nutzt einen deutlich 
cleveren Algorithmus.
Dein Weg ist in etwa so effizient, als würdest du versuchen eine 
Addition (a + b) durch b mal increment von a ersetzen. Das kann 
schneller sein wenn b klein ist, wenn b aber 10000 ist dann ist ein (a + 
b) doch signifikant schneller.
Gast #2805679
Lesenswert?

OK, vielen Dank für die Antworten!

Ich habe schon ein wenig im Internet gesucht, aber werde nicht 
fündig...gibt es irgendwo beschrieben, wie so eine höchst-optimierte 
Berechnung aussieht?

Das würde mich jetzt doch mal interessieren?

Karl Heinz Buchegger schrieb:
> Die Compilerfunktionen arbeiten sich auf Bitebene durch die Zahl durch.
> Eine 32 Bit Multiplikation ist im Prinzip 32 mal eine (32-Bit)Addition
> mit entweder 0(*) oder der Originalzahl und ein wenig SChieben.

Darunter kann ich mir nämlich noch nicht so viel vorstellen.
#2805693
Lesenswert?

MisterX schrieb:
> Ich habe schon ein wenig im Internet gesucht, aber werde nicht
> fündig...gibt es irgendwo beschrieben, wie so eine höchst-optimierte
> Berechnung aussieht?

Lernt man heute noch, wie man mit Papier und Bleistift multipliziert? 
Das geht binär exakt gleich, nur eben mit Bits statt Dezimalziffern.

http://en.wikipedia.org/wiki/Binary_multiplier#Multiplication_basics
Gast #2805700
Lesenswert?

man könnte schon mal bei der multiplikation die Faktoren tauschen damit 
der kleinste wert für die Scheife verwendet wird.

Deine Funktion ist viel langsamer wenn man

1 * 100000

rechnet als wenn man

100000 *  1 rechnet.
#2805707
Lesenswert?

MisterX schrieb:
> OK, vielen Dank für die Antworten!
>
> Ich habe schon ein wenig im Internet gesucht, aber werde nicht
> fündig...gibt es irgendwo beschrieben, wie so eine höchst-optimierte
> Berechnung aussieht?
>
> Das würde mich jetzt doch mal interessieren?

Da ist überhaupt nichts "optimiert". Das funktioniert genau gleich, wie 
du es auch in der Grundschule gelernt hast. Nur deutlich einfacher :-)
Denn im Grunde musst du dir nur merken

  0 mal irgendeine_Zahl     ergibt 0
  1 mal irgendeine_Zahl     ergibt diese Zahl

und der Rest ist genau gleich, wie wenn du am Papier multiplizierst

    34 * 12
    -------
    34
     68
   -----
    428

(nur dass man im Rechner nicht x Zahlen untereinander schreibt, die dann 
alle auf einmal addiert werden, sondern immer gleich addiert.


AVR-Tutorial: Arithmetik8: Multiplikation
#2805788
Lesenswert?

Karl Heinz Buchegger schrieb:
> Fazit: Bei wirklich kleinen Faktoren kann Addition/Subtraktion schneller
> sein.

Und auch in dem Fall sollte man nicht anfangen, das was man meint 
(Multiplikation) durch etwas weniger offensichtliches (Kombination aus 
Additionen) zu ersetzen. Denn das kann auch der optimierende Compiler 
machen, da lässt man das Programm lieber lesbar.
Persönliche Seite #2806223
Lesenswert?

Ganz konkret hier die 32-Bit Multiplikation des GNU avr-gcc für den 
Fall, daß die Hardware keinen MUL-Befehl kennt.

Berechnet wird CC = A * B
 
1
    clr     CC2
2
    clr     CC3
3
    clr     CC0
4
    clr     CC1
5
    rjmp 3f
6
1:  add  CC0,B0  $  adc  CC1,B1  $  adc  CC2,B2  $  adc  CC3,B3
7
2:  lsl  B0      $  rol  B1      $  rol  B2      $  rol  B3
8
3:  lsr  A3      $  ror  A2      $  ror  A1      $  ror  A0
9
    brcs 1b
10
    sbci    A1, 0
11
    brne 2b
12
    sbiw    A2, 0
13
    brne 2b
14
    wmov    C0, CC0
15
    wmov    C2, CC2
16
    ret
 
Die Registerzuteilung ist:
 
1
A  = 22, 23, 24, 25
2
B  = 18, 19, 20, 21
3
CC = 26, 27, 30, 31
4
C  = 22, 23, 24, 25 = A
 
Der Algorithmus entsprich also tatsächlich einer 
Schulbuch-Implementierung.  Er ist auf Codegröße getrimmt, denn die 
Controller, die nicht über MUL verfügen, haben nur seehr wenig Flash.

Die Implementierung verwendet Sequenzen, wie
 
1
    ROR  A0
2
    BRCS 1b
3
    SBCI A1, 0
4
    BRNE 2b
 
die eher unüblich sind und ein C-Compiler kaum erzeugen dürfte.  Dies 
spart zusätzlich ein paar Ticks und Instruktionen ein.

Wie Peter II bereits schrieb, könnte durch Tauschen der Operanden der 
Algroithmus schneller gemacht werden, und zwar können im Mittel 31/6 ≈ 5 
Schleifendurchläufe dadurch gespart werden (zumndest wenn ich mich net 
verrechnet hab).

Der GCC-Code macht das allerdings nicht, weil das zu viel Speicherplatz 
kostst, nämlich rund 16 Instruktionen was den Code rund 70% größer 
machen würde.
Gast #2806275
Lesenswert?

A. K. schrieb:
> MisterX schrieb:
>> Ich habe schon ein wenig im Internet gesucht, aber werde nicht
>> fündig...gibt es irgendwo beschrieben, wie so eine höchst-optimierte
>> Berechnung aussieht?
>
> Lernt man heute noch, wie man mit Papier und Bleistift multipliziert?
> Das geht binär exakt gleich, nur eben mit Bits statt Dezimalziffern.
>
> http://en.wikipedia.org/wiki/Binary_multiplier#Mul...
>
Da gibt es aber deutlich effizientere Multiplikationsalgorithmen.
#2806287
Lesenswert?

BWLer schrieb:
> Da gibt es aber deutlich effizientere Multiplikationsalgorithmen.

In Software ist das der allgemeine Ansatz wenn nicht mindestens eine 
partielle Hardware-Multiplikation zur Verfügung steht. Klar kann man 
noch optimieren, mit early out und dem billigeren Operand auf der 
richtigen Seite. Aber das wäre kaum der nächste Schritt nach seiner 
etwas zu primitiv geratenen Version oben.

Ansonsten darfst du ihm selbstverständlich diese Algorithmen jederzeit 
näher bringen.
Moderator #2806292
Lesenswert?

BWLer schrieb:
> Da gibt es aber deutlich effizientere Multiplikationsalgorithmen.

Dann zeig uns mal eine 32-Bit-Multiplikation mit 32-Bit-Ergebnis für
AVRs ohne Hardware-Multiplizierer, die deutlich effizienter ist als der
drei Beiträge weiter oben von Johann gepostete Code.

Ich bin sehr gespannt :)

P.S.: Ich habe eine leise Ahnung, worauf du hinaus möchtest. Aber ist
das wirklich die Lösung für das vorliegende Problem?
Gast #2806299
Lesenswert?

Yalu X. schrieb:
> BWLer schrieb:
>> Da gibt es aber deutlich effizientere Multiplikationsalgorithmen.
>
> Dann zeig uns mal eine 32-Bit-Multiplikation mit 32-Bit-Ergebnis für
> AVRs ohne Hardware-Multiplizierer, die deutlich effizienter ist als der
> drei Beiträge weiter oben von Johann gepostete Code.

Er ist BWLer, (weg)optimieren ist sein Beruf!
#2806309
Lesenswert?

Nur so aus Neugier:

Woher hast Du diesen Ausdruck?

> source_number = ( source_number - (source_number + source_number));

Schon diesen könnte man nämlich vereinfachen:
1
source_number = source_number - (source_number + source_number);

wird zu
1
source_number = ( source_number - source_number) - source_number;

wird zu
1
source_number = 0 - source_number;

wird zu
1
source_number = -source_number;
#2806332
Lesenswert?

Grundsätzlich: Natürlich kann man Funktionen schreiben, die schneller
sind, als die in den Bibliotheken. Die Leute, die diese implementiert
haben, sind meist extrem gute Programmierer - aber saumäßige Hellseher.
Wenn die eine Routine für die Multiplikation schreiben, dann so, dass
sie praktisch unter allen Umständen funktioniert, wasserfest und
bügelfrei ist.
Was die Leute, natürlich nicht, wissen können ist, dass du z.B. nur
Zahlen mit 0, 1 und 2 multiplizierst. In diesem Falle ist natürlich
Wissen = Mach und eine angepasste Funktion ist immer schneller.

Jemanden
der erstens unter Langeweile leidet
und zweitens sehr genau weiß was er tut
und drittens von speziellen statt allgemeinen Fällen umgeben ist
dem kann ich nur empfehlen: Mach‘s selber.

Der einzige Fall, in dem ich mir so etwas antun würde ist, wenn ich 
Rechenzeitprobleme hätte.
Eine Funktion, die 100000 Mal aufgerufen wird und in der nur ein
Register gesichert wird, dass in meiner Anwendung gar nicht verwendet
wird (kein Hellseher), oder dass ich vorher einmalig sicheren kann -
bringt natürlich einiges an Zeitersparnis.
Gast #2806350
Lesenswert?

wenn du wirklich eine schnelle Multiplikation haben möchtest: mein 
Informatik Prof meint (ich glaube ihm, habe es aber noch nie getestet) 
dass sich alle Grundrechenarten auf Bit-Operationen und Schleifen 
zurückführen lassen. Deine Funktion mit den + und - ist letztendlich 
eine Bitverschiebung mit Schleife, was du mehrere male durchlaufen 
lässt. Daher ist deine deutlich langsamer als die die von Leuten 
entwickelt wurden die sonst nix zu tun haben (sprich Mathematiker)
#2806385
Lesenswert?

Die Idee mit der Tabelle ist wirklich toll.
Eine 32x32 Bit Multiplikation bringt Werte zwischen 0 und 4294967296
Die Preisfrage lautet somit: Wie komme ich zum Index des Ergebnisses
ohne ...

Übrigens coco jack dein Prof. hat natürlich recht wie auch die
Ausführungen von Karl Heinz Buchegger implizieren.
Persönliche Seite #2806413
Lesenswert?

Peter II schrieb:
> Sebastian S. schrieb:
>> Die Preisfrage lautet somit: Wie komme ich zum Index des Ergebnisses
>> ohne ...
>
> einfach beide zahlen zu einer 64bit zahlen zusamensetzen?
>
> int64 == int32 << 32 | int32

Genau.

Und danach muss man nur noch das Ergebnis aus dem 134217728 Tera-Byte 
großen Speicher lesen.  In Tera-Byte Platten und schön gestapelt, ist 
das ein Quader mit 512  512  512 Platten...

EIn Glück, daß es die 3. Binomische Formel gibt, damit braucht man nur 
noch 72 Giga-Byte (2^33 * 9 Bytes).

Antwort schreiben

Bitte melde dich an, um einen Beitrag zu schreiben.

oder

Mit Google-Account einloggen

Die Registrierung ist kostenlos und dauert nur eine Minute.

Jetzt registrieren