Effiziente Division durch Multiplikation

#5298591
Lesenswert?

Guten Abend zusammen,

ich will auf dem ARM Cortex M3/M4 eine Division durch eine effizientere 
Multiplikation ersetzen. Bei dem erwarteten Wertebereich habe ich so 
keine Genauigkeitsverluste zu verzeichnen. Momentan bin ich mir jedoch 
unsicher, ob mir die Integer-Promotion bei negativem Zähler keinen 
Strick drehen kann.

Gegeben ist ein Bruch a/b. b ändert sich selten. b ist positiv. Der 
Übersicht halber habe ich alles in eine Funktion geschrieben:
1
int32_t divideByB(int32_t a)
2
{
3
    // Vorbereitung
4
    const uint32_t b = 12345L;
5
    assert( b!=0 );
6
    uint32_t oneDividedByB_q32;
7
    if( b == 1 )
8
        oneDividedByB_q32 = UINT32_MAX; // Naeherung
9
    else
10
        oneDividedByB_q32 = (1ULL<<32)/b; // Exakt
11

12
    // Berechnung
13
    int64_t result = (int64_t) a * oneDividedByB_q32; // Kann nicht ueberlaufen, da 31 Bit * 32 Bit
14
    return result/(1LL<<32); // Nur vorderes Register zurueckgeben
15
}

Die Hilfsvariable wurde q32 gewählt, damit das fertige Ergebnis direkt 
in einem Register steht und der Bitshift erst gar nicht ausgeführt 
werden muß. Bei der Berechnung der Hilfsvariablen wird der Nenner zu 
UINT64 befördert und die Division ausgeführt (was lange dauert - hier 
aber nicht weiter stört).

a ist Int64. oneDividedByB_q32 passt in ein Int64, wird also zu einem 
Int64 befördert und die Multiplikation ausgeführt. Wenn b!=1, ist das 
Ergebnis immer identisch mit der direkten Division.

Habe ich das richtig zusammengepuzzlet, oder habe ich irgendeinen 
möglichen Überlauf übersehen?

Viele Grüße
W.T.
Beitrag #5298648 wurde von einem Moderator gelöscht.
#5298797
Lesenswert?

Mein

Joe F. schrieb:
> würde ich eher
>
> return (result >> 32);
>
> hinschreiben

Hmneee, das Ergebnis wäre implementation defined. Ich will das 
Vorzeichenbit durchaus mit Sicherheit behalten.

Dr. Sommer schrieb:
> Und das ist wirklich schneller als die 12 Takte, die Cortex-M3/4 für
> eine Integer-Division braucht?

Unmittelbar: Kaum. Mittelbar kann ich im Faktor oneDividedByX_q32 
allerdings das Ergebnis einer längeren Rechnung zwischenspeichern, und 
dann will ich den Teil mit der Multiplikation auch "noch eben" 
mitnehmen, weil ich mir dann in der "schnellen" Funktion die 
Fallunterscheidung mit O sparen kann.
#5298890
Lesenswert?

Bitte noch einmal zurück zu meiner Frage:

1. Ich bin mir bei der Multiplikation Uint32_t/Int32 unsicher, ob ich 
die Integer-Promotion-Regeln richtig ausgelegt habe - kann ich hier 
immer sicher sein, daß nichts überlaufen kann?

2. Läßt sich das Vorgehen auf einen Divisor mit Vorzeichen erweitern?
Gast #5298902
Lesenswert?

Walter T. schrieb:
> Bei dem erwarteten Wertebereich habe ich so
> keine Genauigkeitsverluste zu verzeichnen.
1
#include <stdio.h>
2
#include <assert.h>
3
#include <stdint.h>
4
#include <inttypes.h>
5

6
int32_t divideByB(int32_t a, uint32_t b)
7
{
8
    // Vorbereitung
9
    assert( b!=0 );
10
    uint32_t oneDividedByB_q32;
11
    if( b == 1 )
12
        oneDividedByB_q32 = UINT32_MAX; // Naeherung
13
    else
14
        oneDividedByB_q32 = (1ULL<<32)/b; // Exakt
15

16
    // Berechnung
17
    int64_t result = (int64_t) a * oneDividedByB_q32; // Kann nicht ueberlaufen, da 31 Bit * 32 Bit
18
    return result/(1LL<<32); // Nur vorderes Register zurueckgeben
19
}
20

21

22
int main (void) {
23
  int32_t a = 10;
24
  uint32_t b = 5;
25
  printf ("a/b=%" PRId32 "\ndivideByB(%" PRId32 ", %" PRIu32") = %" PRId32 "\n", a/b, a, b, divideByB (a, b));
26
}

ergibt:
1
a/b=2
2
divideByB(10, 5) = 1
Hmm...
#5298931
Lesenswert?

Dr. Sommer schrieb:
> a/b=2
> divideByB(10, 5) = 1Hmm...

Und das Schlimme: Ich verstehe nicht warum.
1
int32_t divideByB(int32_t a)
2
{
3
    // Vorbereitung
4
    const uint32_t b = 5L;
5
    assert( b!=0 );
6
    uint32_t oneDividedByB_q32;
7
    if( b == 1 )
8
        oneDividedByB_q32 = UINT32_MAX; // Naeherung, Liefert immer a - 1 zurueck
9
    else
10
        oneDividedByB_q32 = (1ULL<<32)/b; // Exakt
11

12
    printf("oneDividedByB = %u ", oneDividedByB_q32);
13

14
    // Berechnung
15
    int64_t result = (int64_t) a * (uint64_t) oneDividedByB_q32; // Kann nicht ueberlaufen, da 31 Bit * 32 Bit
16
    printf("zwischen = %lli ", result);
17
    printf("1<<32=%lli ", 1LL<<32);
18
    return (result/(1LL<<32)); // Nur vorderes Register zurueckgeben
19
}
20

21
int main(void)
22
{
23
    printf("result=%li", divideByB(10));
24
    return 1;
25
}
1
oneDividedByB = 858993459 zwischen = 8589934590 1<<32=4294967296 result=1
Gast #5298945
Lesenswert?

Walter T. schrieb:
> Und das Schlimme: Ich verstehe nicht warum.
Ist doch klar, bei der ersten Division wird abgerundet, weil (1<<32) 
nicht glatt durch 5 teilbar ist. Somit ist das Ergebnis der 
Multiplikation etwas zu klein und wird wieder abgerundet. Man könnte die 
beiden Divisionen korrekt rundend ausführen aber auch dann müssten sich 
Werte finden lassen bei denen es nicht passt.
Wenn du weniger als 22 signifikante Binär-Stellen hast, könntest du 
float verwenden, da dauert die Multiplikation 1 Takt.
Gast #5298946
Lesenswert?

Du musst vor dem Shift, oder vor der Multiplikation runden durch 
Addition eines Offsets. Der shift rundet nämlich ab. Hab mir jetzt aber 
keine Gedanken über den negativen Bereich gemacht. Ich hab die Methode 
früher für vorzeichenlose Divisionen auf avrs benutzt.
#5299181
Lesenswert?

avr schrieb:
> oder vor der Multiplikation runden durch Addition eines Offsets.

Mit dem Offset riskiere ich natürlich wieder einen Überlauf, wenn ich a 
nicht begrenze. Für den einfachen Fall mit x/b mit int32_t x, b ist die 
Multiplikation mit dem Kehrwert dann einfach zu kompliziert, weil 
Fallunterscheidungen nötig sind.

Schade - aber nicht schlimm. Wie ja schon oben richtig geschrieben 
wurde, kostet mich die nackte Division zweier 32-Bit-Zahlen gerade mal 
maximal 12 Takte.


Generell suche ich auch noch immer nach einer sinnvollen allgemeinen 
Lösung zum allgemeineren Problem
1
int32_t r, a, z, n
2

3
[...]
4

5
r = (int64_t) a*z/n;
(siehe Beitrag "Einflußfaktoren zur Rechendauer bei Divisionen"), aber bislang 
konnte ich es jetzt immer irgendwie umgehen.
Persönliche Seite #5299262
Lesenswert?

Walter T. schrieb:
> Generell suche ich auch noch immer nach einer sinnvollen allgemeinen
> Lösung zum allgemeineren Problem

Ist nicht so ganz trivial, zumindest wenn man sich den Code im GCC dazu 
anschaut.

Und einfach mit dem "Kehrwert" zu multiplizieren ist auch nicht die 
Lösung, zumindest wenn das Ergebnis das gleiche wie bei einer Division 
sein soll.  Eine unsigned Division durch 7 übersetzt gcc zum Beispiel 
so:
1
div_7:
2
  ldr  r3, .L3
3
  umull  r2, r1, r0, r3
4
  sub  r0, r0, r1
5
  add  r0, r1, r0, lsr #1
6
  lsr  r0, r0, #2
7
  bx  lr
8
  .align  2
9
.L3:
10
  .word  613566757
Es gibt also noch zusätzliche Arithmetik, um das Ergebnis anzupassen.

Ohne diese Anpassung wird dein Ergebnis i.d.R. nicht mit dem Quotienten 
einer entsprechenden Division übereinstimmen, und deine Hausaufgabe ist 
dann, den (maximalen) Fehler abzuschätzen und zu entscheiden ob dieser 
in der Anwendung tolerierbar ist.

Was du momentan versuchst ist eine Art JIT Ansatz, jedoch ohne über den 
eigentlichen Algorithmus zu verfügen.  Und selbst mit dessen Kenntnis 
müsste immer noch eine Verzweigung anhand des Divisors geschehen, der 
zusätzliche Laufzeit kostet.  Oder der Algorithmus wied so allgemein 
gehelten, dass er für alle Divisoren gültig ist, was u.U. einen 
deitlich größeren Overhead bedeutet.

Überhaupt scheint das nur sinnvoll, wenn alle Divisoren bekannt sind, 
und dann ein switch-case eine schnelle Verzweigung zum Code hinbekommt. 
Oder wenn alle Divisoren eine gemeinsame Struktur haben, welche sich 
dann in einem einheitlichen Algorithmus niederschlägt.
#5299621
Lesenswert?

Mit 8 Stunden Abstand fällt mir auf: Eigentlich ist das Ergebnis gar 
nicht so schlecht. Über den kompletten Wertebereich liegt der Fehler im 
Vergleich zur Rechnung mit double-Genauigkeit zwischen -1.5 und +0.5. Im 
16-Bit-Bereich sogar zwischen -1 und 0.

Bei 10 und 5 sieht es nur drastisch falsch aus. Und exakt ist es 
natürlich wirklich nicht.

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