Forum: Projekte & Code GCC Inline Assembler Multiplikationen


von Dirk S. (disc)


Angehängte Dateien:

Lesenswert?

Hallo Leute,
da ich in einem Projekt von mir viele Berechnungen mit Zahlen 
unterschiedlichen Formates habe, habe ich mir mal ein paar 
Inline-Assembler Funktionen geschrieben, um diese ein wenig zu 
optimieren.

Da ich nur bescheidene Assembler Kenntnisse habe, wäre es schön, wenn 
der eine oder andere mal einen Blick darauf werden könnte, ob alles so 
in Ordnung ist und ob man noch etwas optimieren könnte.
Evtl. kann man die Funktionen noch als inline deklarieren.
Ein Modul mit ein paar Testfunktionen ist auch beigefügt.

Ansonsten stehen die Funktionen für alle zu Benutzung frei.

Gruß

Dirk

von (prx) A. K. (prx)


Lesenswert?

"=&r" ist nur erforderlich, wenn das Ergebnis nicht im gleichen Register 
stehen darf wie die Operanden. Wenn das beispielsweise der Befehl nicht 
zulässt, oder wenn die Operanden stückweise verarbeitet werden. 
Mindestens bei den 8x8=>16 Multiplikationen ist "=r" ausreichend.

Ich würde dringend empfehlen, die kurzen Funktionen ins .h File zu 
stellen und mit "static inline" zu markieren. Dann kann der Compiler sie 
ohne Aufruf direkt verwenden, was die Sache grad bei den 
8x8-Multiplikationen erheblich beschleunigt.

R2 gehört zu den Registern, die gesichert werden müssen, daher wird der 
Compiler bei dessen Verwendung PUSH/POP einschieben. Mir scheint der in 
Funktionen nicht zu sichernde Registerbereich sinnvoller, also 
beispielsweise R31.

von Johann L. (gjlayde) Benutzerseite


Lesenswert?

Wenn du "optimieren" sagst, muss man schon wissen, was zu optimieren 
ist...

-- Laufzeit?
-- Codedichte?
-- Entwicklungszeit?
-- Portabilität?
-- ...

So wie A. schon schrieb, ist R2 ungünstig als Clobber-Register. Besser 
definierst du eine lokale Variable, und lässt den Compiler die 
Allokierung übernehmen:
1
int32_t mul_s16xu8_s32(int16_t nNumber1, uint8_t ucNumber2)
2
{
3
   int32_t lResult;
4
   uint8_t zero = 0;
5
6
   asm (
7
         "clr    %C[lResult]" "\n\t"
8
         "clr    %D[lResult]" "\n\t"
9
         "mul    %A[nNumber1], %[ucNumber2]" "\n\t" // al * b
10
         "movw   %A[lResult], r0" "\n\t"
11
         "mulsu  %B[nNumber1], %[ucNumber2]" "\n\t" // (signed)ah * b
12
         "sbc    %D[lResult], %[zero]" "\n\t"
13
         "add    %B[lResult], r0" "\n\t"
14
         "adc    %C[lResult], r1" "\n\t"
15
         "adc    %D[lResult], %[zero]" "\n\t"
16
         "clr    __zero_reg__" "\n\t"
17
         : [lResult] "=&r" (lResult)
18
         : [nNumber1] "a" (nNumber1), [ucNumber2] "a" (ucNumber2), [zero] "r" (zero)
19
       );
20
21
   return lResult;
22
}

Wie du siehst, ist das asm auch nicht mehr volatile: Wenn das Ergebnis 
nicht gebraucht wird, darf der Compiler den kompletten asm-Schnippel in 
die Tonne werfen, denn wir haben alle Nebenwirkungen beschrieben!

8x8=16 kann gcc und macht kein schlecheren Code als deine Asm-Stücke. Er 
ist sogar besser, weil er's inline macht und nicht als Call.

Bei mul_u16xu16_u32, mul_u32xu16_u32 und mul_u32xu8_u32 brauchst du kein 
zero-Register. Du kannst ebenso R1 aka _zero_reg_ nehmen (natürlich an 
den verwendeten Stellen nach vorherigen clr). Ok, braucht hier und da 
nen Tick und 2 Byte Code mehr...

Leider ist es mit dem momentanen AVR-Backend nicht möglich, die 
Schnippel als taransparente Calls umzusetzen :-( Mit besseren 
Constraint-Definitionen in avr-gcc wäre das problemlos möglich und 
könnte sehr gewinnbringend eingesetzt werden.

Johann

von (prx) A. K. (prx)


Lesenswert?

Johann L. schrieb:

> So wie A. schon schrieb, ist R2 ungünstig als Clobber-Register. Besser
> definierst du eine lokale Variable, und lässt den Compiler die
> Allokierung übernehmen:

Nur kostet dann der =0 Löschbefehl. Wird direkt ein passenderes Register 
als R2 verwendet, dann kostet es zumindest in separater Funktion 
überhaupt nichts.

von Johann L. (gjlayde) Benutzerseite


Lesenswert?

A. K. schrieb:
> Johann L. schrieb:
>
>> So wie A. schon schrieb, ist R2 ungünstig als Clobber-Register. Besser
>> definierst du eine lokale Variable, und lässt den Compiler die
>> Allokierung übernehmen:
>
> Nur kostet dann der =0 Löschbefehl. Wird direkt ein passenderes Register
> als R2 verwendet, dann kostet es zumindest in separater Funktion
> überhaupt nichts.

Ich versteh jetzt nicht was du damit meinst. Die 0 muss ja irgendwie ins 
Register kommen. Von nix kommt eben nix. Und wenn gcc weiß, daß die 0 
rein soll, und er sie schon irgendwo hat, hat er so die Chance, das 
Wissen zu nutzen. Hier mal ein konstruiertes Beispiel:
1
char x;
2
3
void foo (void)
4
{
5
    
6
    if (((uint8_t) (x+1)) == 0)
7
    {
8
        uint8_t zero = 0;
9
        
10
        asm (" ; x=%[x], zero=%[zero]" : [x] "+r" (x) : [zero] "r" (zero));
11
    }
12
}

Das wird zu
1
foo:
2
  lds r25,x   ;  x, x   ;  10  *movqi/4  [length = 2]
3
  mov r24,r25   ;  tmp42, x   ;  35  *movqi/1  [length = 1]
4
  subi r24,lo8(-(1))   ;  tmp42,   ;  11  addqi3/2  [length = 1]
5
  brne .L7   ; ,   ;  13  branch  [length = 1]
6
/* #APP */
7
   ; x=r25, zero=r24   ;  x, tmp42
8
/* #NOAPP */
9
  sts x,r25   ;  x, x   ;  23  *movqi/3  [length = 2]
10
.L7:
11
  ret   ;  37  return  [length = 1]

D.h. weil gcc weiß, das in R24 schon eine 0 drinne ist, verwendet er 
sie.

Johann

von (prx) A. K. (prx)


Lesenswert?

Johann L. schrieb:

> Ich versteh jetzt nicht was du damit meinst.

War Blödsinn. Sorry, ich hatte das nicht komplett gelesen, sondern die 
Variable als direkten Ersatz für R2 verstanden.

von Johann L. (gjlayde) Benutzerseite


Lesenswert?

...und der Vollständigkeit halber gehören dann auch 16=8*16 dazu:
1
static inline uint16_t 
2
mul_u16xu8_u16 (uint16_t unNumber1, uint8_t ucNumber2)
3
{   
4
    uint16_t unResult;
5
6
    asm (
7
        "mul    %A[unNumber1], %[ucNumber2]" "\n\t" // al * b
8
        "movw   %A[unResult], r0"            "\n\t"
9
        "mul    %B[unNumber1], %[ucNumber2]" "\n\t" // ah * b
10
        "add    %B[unResult], r0"            "\n\t"
11
        "clr    __zero_reg__"
12
            : [unResult] "=&r" (unResult)
13
            : [unNumber1] "r" (unNumber1), [ucNumber2] "r" (ucNumber2)
14
    );
15
16
    return unResult;
17
}
18
19
// etc.

von Dirk S. (disc)


Angehängte Dateien:

Lesenswert?

Hallo Leute,
vielen Dank erst mal für die Antworten und die Tipps.

Ich habe ein paar von den Vorschlägen eingebaut und hier wieder 
angehängt. Die Funktionen sind alle als static inline im Header-File 
deklariert. Das hat evtl. den kleinen Nachteil, dass sie vom Compiler 
immer "ge-inlined" ;-) werden, also etwas mehr Platz brauchen.

> 8x8=16 kann gcc und macht kein schlecheren Code als deine Asm-Stücke. Er
> ist sogar besser, weil er's inline macht und nicht als Call.
Hm, das wusste ich gar nicht. Ist das bei allen Optimierungsstufen so?
Ich dachte, errechnet dann 8bit x 8bit -> 8bit und konvertiert danach 
erst in 16bit?!

Die 16x8 -> 16 Geschichte gehe ich evtl. in den nächsten Tagen mal an 
und reiche die dann nach.

Gruß

Dirk

von Johann L. (gjlayde) Benutzerseite


Lesenswert?

Dirk Schmidt schrieb:

> Ich habe ein paar von den Vorschlägen eingebaut und hier wieder
> angehängt. Die Funktionen sind alle als static inline im Header-File
> deklariert. Das hat evtl. den kleinen Nachteil, dass sie vom Compiler
> immer "ge-inlined" ;-) werden, also etwas mehr Platz brauchen.

Falls man Funktionen oft braucht, kann man sie ja in normale Funktionen 
einpacken.

>> 8x8=16 kann gcc und macht kein schlecheren Code als deine Asm-Stücke. Er
>> ist sogar besser, weil er's inline macht und nicht als Call.
> Hm, das wusste ich gar nicht. Ist das bei allen Optimierungsstufen so?

Die Pattern "mulqihi3" und "umulqihi3" für 16=8x8 sind zumindest im 
avr-Backend beschrieben. Sie sind daher für alle O-Stufen gültig.

http://gcc.gnu.org/viewvc/trunk/gcc/config/avr/avr.md?revision=152958&view=markup

> Ich dachte, errechnet dann 8bit x 8bit -> 8bit und konvertiert danach
> erst in 16bit?!

Das wäre der Fall mit -mint8 wo ein int nur 8 Bit hat.

> Die 16x8 -> 16 Geschichte gehe ich evtl. in den nächsten Tagen mal an
> und reiche die dann nach.

Schade daß man das alles von Hand machen muss... Im AVR-Backend würde 
sich sowas ganz gut machen. Muss nur jemand einbauen, der da mitspielen 
darf wie Jörg.

von Dirk S. (disc)


Angehängte Dateien:

Lesenswert?

Hallo,
hier noch die fehlenden 16x8->16 Funktionen.

>Schade daß man das alles von Hand machen muss... Im AVR-Backend würde
>sich sowas ganz gut machen. Muss nur jemand einbauen, der da mitspielen
>darf wie Jörg.
Wenn man ein paar dieser Optimierungen einbauen würde, könnte man 
bestimmt noch etwas an Geschwindigkeit und Platz herausholen.
Ich hab das bei mir gemerkt. Die Berechnungen sind schneller und der 
Code kleiner (mit -O3) geworden.

Vielleicht hat der eine oder andere ja auch noch ähnliche Makros für 
Division in der Schublade liegen?
Wobei ich jetzt gar nicht weiss, wie z.B. eine 32 : 8 Division intern 
vom GCC gerechnet wird!? Als 32 : 8 oder 32 : 32?

Gruß

Dirk

von (prx) A. K. (prx)


Lesenswert?

Nochmal: Bei den einfachen Versionen ist "=&r" nicht sinnvoll, "=r" 
reicht aus und erlaubt es dem Compiler, die gleichen Register zu 
verwenden.

von Johann L. (gjlayde) Benutzerseite


Lesenswert?

Hinweis:

Ab avr-gcc 4.7 kann der Compiler selbst das Produkt erweiter, d.h. bei 
Code wie
1
long mul (shart a, short b)
2
{
3
    return (long) a * b;
4
}
wird nicht vor der Multiplikation auf 32 Bits erweitert, sondern während 
der Operation.

Unterstützt werden:

16 = 16 × 8
16 =  8 × 8

32 = 32 × 16
32 = 32 × 8
32 = 16 × 16
32 = 16 × 8
32 =  8 × 8

für signed und unsigned, allerdings nur bei vorhandener MUL-Instruktion.

Diese erweiternde Multiplikation kann auch für Divisionen mit konstantem 
Divisior Verwendung finden:
1
unsigned int div16_10 (unsigned int x)
2
{
3
    return x / 10;
4
}
5
6
unsigned char div8_10 (unsigned char x)
7
{
8
    return x / 10;
9
}
wird zB mit -O2 für ATmega8 übersetzt zu
1
div16_10:
2
  movw r18,r24
3
  ldi r26,lo8(-13107)
4
  ldi r27,hi8(-13107)
5
  rcall __umulhisi3 ; 32 = 16 * 16
6
  lsr r25
7
  ror r24
8
  lsr r25
9
  ror r24
10
  lsr r25
11
  ror r24
12
  ret
13
14
div8_10:
15
  ldi r25,lo8(-51)
16
  mul r24,r25
17
  mov r24,r1
18
  clr __zero_reg__
19
  lsr r24
20
  lsr r24
21
  lsr r24
22
  ret

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.