Forum: Mikrocontroller und Digitale Elektronik GCC Inline Assembler: input-output Operanden, schlechterer Code


von 3? (Gast)


Lesenswert?

Macht es einen Unterschied für den (inline) Assembler, ob Operanden
  - als input-output (constraint modifier "+"), oder
  - als input-output UND input
gelistet sind?

Kontext:

Ich nutze folgende strncmp_P-ähnliche Funktion in einem C Programm. 
Diese hat für mich den Vorteil, dass
  - der Parameter explizit den __flash Adressraum verwendet (statt 
strncmp_P generic AS, vermeidet Warnung)
  - die Funktion relativ effizient inlined wird (kein call overhead, 
keine parameter register)
  - nur einen uint8_t als Parameter akzeptiert (statt size_t), und
  - nur einen int8_t zurueck gibt (statt int)

Die erste Variante hat die Parameter sowohl in der output operanden 
Liste (input-output) als auch in der input(-only) operanden Liste 
aufgeführt.
1
static int8_t
2
strncmp_P_(const char *s1, const __flash char *s2, uint8_t n)
3
{
4
    uint8_t ret;
5
    asm ("\n\t"
6
        "1:                         \n\t"
7
        "   subi   %[n], 1          \n\t"
8
        "   brcs   2f               \n\t"
9
        "   ld     %[ret], %a[s1]+  \n\t"
10
        "   lpm    r0, %a[s2]+      \n\t"
11
        "   sub    %[ret], r0       \n\t"
12
        "   brne   3f               \n\t"
13
        "   tst    r0               \n\t"
14
        "   brne   1b               \n\t"
15
        "2:                         \n\t"
16
        "   sub    %[ret], %[ret]   \n\t"
17
        "3:                         \n\t"
18
        : [s1] "+xy" (s1), [s2] "+z" (s2), [n] "+d" (n), [ret] "=r" (ret)
19
        : "[s1]" (s1), "[s2]" (s2), "[n]" (n)
20
        : "cc", "r0");
21
    return ret;
22
}

Die zweite Variante hat die Parameter nur in der output operanden 
Liste (input-output) aufgeführt.
1
static int8_t
2
strncmp_P_(const char *s1, const __flash char *s2, uint8_t n)
3
{
4
    uint8_t ret;
5
    asm ("\n\t"
6
        "1:                         \n\t"
7
        "   subi   %[n], 1          \n\t"
8
        "   brcs   2f               \n\t"
9
        "   ld     %[ret], %a[s1]+  \n\t"
10
        "   lpm    r0, %a[s2]+      \n\t"
11
        "   sub    %[ret], r0       \n\t"
12
        "   brne   3f               \n\t"
13
        "   tst    r0               \n\t"
14
        "   brne   1b               \n\t"
15
        "2:                         \n\t"
16
        "   sub    %[ret], %[ret]   \n\t"
17
        "3:                         \n\t"
18
        : [s1] "+xy" (s1), [s2] "+z" (s2), [n] "+d" (n), [ret] "=r" (ret)
19
        : // **no input-only operands**
20
        : "cc", "r0");
21
    return ret;
22
}

Beide Varianten werden in einer längeren Funktion verwendet und korrekt 
geinlined. Allerdings benötigt die erste Variante weniger Register und 
ist damit kürzer (push, pop) als die zweite Variante.

Wieso?

Es hängt offensichtlich mit der zusätzlichen Auflistung als input 
Operanden zusammen.
Ist das falsch von mir bzw. sage ich damit (fälschlicherweise), dass die 
Register unverändert bleiben (dafür hätte ich ja input-output constraint 
modifier "+" verwendet)?

Oder ist es nur Zufall, weil der inline-Asm andere (hier ungünstigere) 
Register auswählt?

Danke,
???

von 3? (Gast)


Lesenswert?

3? schrieb:
> Beide Varianten werden in einer längeren Funktion verwendet und korrekt
> geinlined. Allerdings benötigt die erste Variante weniger Register und
> ist damit kürzer (push, pop) als die zweite Variante.

Damit meine ich natürlich, dass strncmp_P_ Funktion nur einmal verwendet 
wird (in einer anderen, längeren Funktion). Aber wenn ich mit der erste 
Variante kompiliere, ist das Programm kürzer, als wenn ich die zweite 
Variante kompiliere.

von TO (Gast)


Lesenswert?

3? schrieb:
> Ist das falsch von mir bzw. sage ich damit (fälschlicherweise), dass die
> Register unverändert bleiben (dafür hätte ich ja input-output constraint
> modifier "+" verwendet)?

Ich glaube, ich habe in einem anderen Fall genau das gleiche Problem. 
Ein im asm statement verändertes Register wird nicht neu geladen, obwohl 
eine schreibende Abhängigkeit angegeben ist:
1
static void fun(volatile uint8_t *samples)
2
{
3
    uint8_t n;
4
    asm volatile (
5
        "ld   %[n], %a[ptr]+ \n"
6
        : [ptr] "=z" (samples), [n] "=&r" (n)
7
        : "0" (samples)
8
        : /* no clobbers */);
9
    samples[0] = n;
10
}

Das ergibt:
1
         ld  r24, Z+
2
         st  Z, r24

Der gleiche Code wird mit "+" output constraint generiert:
1
static void fun(volatile uint8_t *samples)
2
{
3
    uint8_t n;
4
    asm volatile (
5
        "ld   %[n], %a[ptr]+ \n"
6
        : [ptr] "+z" (samples), [n] "=&r" (n)
7
        : /* no input operands */
8
        : /* no clobers */);
9
    samples[0] = n;
10
}

Korrekt wäre z.B.
1
         movw r22, r30  ; backup Z
2
         ld   r24, Z+
3
         movw r30, r22  ; restrore Z
4
         st   Z, r24

AVR-GCC aus den Debian repositores (gcc-avr 5.4.0+Atmel3.6.1-2), sollte 
gleich sein zu AVR-Studio:
1
$ avr-gcc -dumpversion
2
5.4.0

Kompiliert wird mit
1
avr-gcc -mmcu=atxmega32a4u -DF_CPU=32000000UL -DNDEBUG -O2 -MD -Wall -Wextra -std=gnu11 -g -mrelax -fdiagnostics-color -fdata-sections -ffunction-sections -fshort-enums -Wall -Wextra -Werror-implicit-function-declaration -Wmissing-prototypes -Wpointer-arith -Wstrict-prototypes -Wswitch-enum -Waddr-space-convert -Winvalid-memory-model -Wshadow -Wtype-limits -ftree-loop-distribute-patterns  -o foo.o -c foo.c

Ich fände es großartig, hier eine Lösung zu haben...

von TO (Gast)


Lesenswert?

In der avr-libc wird teilweise genau das gleiche gemacht, aber es 
funktioniert. avr/pgmspace.h:
1
#define __LPM_word_tiny__(addr)             \
2
(__extension__({                            \
3
    uint16_t __addr16 = (uint16_t)(addr) + __AVR_TINY_PM_BASE_ADDRESS__; \
4
    uint16_t __result;                      \
5
    __asm__                                 \
6
    (                                       \
7
        "ld %A0, z+"     "\n\t"             \
8
        "ld %B0, z"      "\n\t"             \
9
        : "=r" (__result), "=z" (__addr16)  \
10
        : "1" (__addr16)                    \
11
    );                                      \
12
    __result;                               \
13
}))

von Johann L. (gjlayde) Benutzerseite


Lesenswert?

TO schrieb:
> Ich glaube, ich habe in einem anderen Fall genau das gleiche Problem.
> Ein im asm statement verändertes Register wird nicht neu geladen, obwohl
> eine schreibende Abhängigkeit angegeben ist:
>
>
1
> static void fun(volatile uint8_t *samples)
2
> {
3
>     uint8_t n;
4
>     asm volatile (
5
>         "ld   %[n], %a[ptr]+ \n"
6
>         : [ptr] "=z" (samples), [n] "=&r" (n)
7
>         : "0" (samples)
8
>         : /* no clobbers */);
9
>     samples[0] = n;
10
> }
> Das ergibt:
1
>          ld  r24, Z+
2
>          st  Z, r24

Soweit korrekt.

> Der gleiche Code wird mit "+" output constraint generiert:
1
> static void fun(volatile uint8_t *samples)
2
> {
3
>     uint8_t n;
4
>     asm volatile (
5
>         "ld   %[n], %a[ptr]+ \n"
6
>         : [ptr] "+z" (samples), [n] "=&r" (n)
7
>         : /* no input operands */
8
>         : /* no clobers */);
9
>     samples[0] = n;
10
> }

Das ist gleichbedeutend mit dem obigen Code:  samples ist Input und 
Output, und beide sind im gleichen Register "z".

> Korrekt wäre z.B.
1
>          movw r22, r30  ; backup Z
2
>          ld   r24, Z+
3
>          movw r30, r22  ; restrore Z
4
>          st   Z, r24

Nein, das wäre falsch.

samples ist Ouput-Operand aus "z", d.h. im weiteren muss der Wert aus Z 
für samples genommen werden, also der durch Post-Increment erhöhte.

> Ich fände es großartig, hier eine Lösung zu haben...

Also eine bessere Verständnis der Semantik von Inline-Asm.

Wenn das ursprüngliche samples weiter verwendet werden soll, dann zum 
Beispiel so:
1
    uint8_t n;
2
    volatile uint8_t *tmp;
3
    asm volatile (
4
        "ld   %[n], %a[ptr]+ \n"
5
        : [ptr] "=z" (tmp), [n] "=r" (n)
6
        : "0" (samples));
7
    samples[0] = n;
oder so:
1
    uint8_t n;
2
    volatile uint8_t *tmp = samples;
3
    asm volatile (
4
        "ld   %[n], %a[ptr]+ \n"
5
        : [ptr] "+z" (tmp), [n] "=r" (n));
6
    samples[0] = n;
Es wird dann das veränderte Z nach tmp geschrieben, nicht zurück nach 
samples.

Außerdem braucht's hier für n kein early-clobber.

von Johann L. (gjlayde) Benutzerseite


Lesenswert?

TO schrieb:
> In der avr-libc wird teilweise genau das gleiche gemacht, aber es
> funktioniert. avr/pgmspace.h:

Da muss gekennzeichnet werden, dass Z (also __addr16) durch das asm 
verändert wird.  Es wird in der Folge aber nicht mehr verwendet.

Auch in dem Beispiel hätte man
1
"=z" (__addr16) : "1" (__addr16)
kürzer schreiben können als
1
"+z" (__addr16) :

Obgleich __addr16 nicht weiter verwendet wird, muss Z bei den Outputs 
auftauchen.  Ansonsten würde man dem Compiler sagen, dass Z nicht 
verändert wird, was nicht stimmt.

: Bearbeitet durch User
von TO (Gast)


Lesenswert?

Johann L. schrieb:
> TO schrieb:
>> Korrekt wäre z.B.
1
>>          movw r22, r30  ; backup Z
2
>>          ld   r24, Z+
3
>>          movw r30, r22  ; restrore Z
4
>>          st   Z, r24
>
> Nein, das wäre falsch.
>
> samples ist Ouput-Operand aus "z", d.h. im weiteren muss der Wert aus Z
> für samples genommen werden, also der durch Post-Increment erhöhte.

Aha! Stimmt, die output Operanden sind lvalues.
Herzlichen Dank!

von Johann L. (gjlayde) Benutzerseite


Lesenswert?

TO schrieb:
> In der avr-libc wird teilweise genau das gleiche gemacht, aber es
> funktioniert. avr/pgmspace.h:
1
> #define __LPM_word_tiny__(addr)             \
2
> (__extension__({                            \
3
>     uint16_t __addr16 = (uint16_t)(addr) + __AVR_TINY_PM_BASE_ADDRESS__; 
4
> \
5
>     uint16_t __result;                      \
6
>     __asm__                                 \
7
>     (                                       \
8
>         "ld %A0, z+"     "\n\t"             \
9
>         "ld %B0, z"      "\n\t"             \
10
>         : "=r" (__result), "=z" (__addr16)  \
11
>         : "1" (__addr16)                    \
12
>     );                                      \
13
>     __result;                               \
14
> }))

Dieses Makro ist nicht nur überflüssig, sondern auch falsch!

In der aktuellen Version der avr-libc ist dieses Makro nicht enthalten, 
und das Log verzeichnet auch kein Entfernen oder Hinzufügen:

http://svn.savannah.nongnu.org/viewvc/avr-libc/trunk/avr-libc/include/avr/pgmspace.h?view=markup
http://svn.savannah.nongnu.org/viewvc/avr-libc/trunk/avr-libc/include/avr/pgmspace.h?view=log

Das Makro ist falsch, weil bereits der Compiler den Offset von 0x4000 
zur Adressberechnung aller mit progmem attributierten Objekte addiert.

Im Gegensatz zu "normalen" AVRs kann man also solche Objekte per vanilla 
C/C++ zugreifen:
1
#include <avr/pgmspace.h>
2
3
const char textP[] PROGMEM = "Text";
4
const char *p;
5
6
char setp_read_progmem (uint8_t i)
7
{
8
    p = textP;
9
    return textP[i];
10
}
1
setp_read_progmem:
2
  ldi r20,lo8(textP+16384)
3
  ldi r21,hi8(textP+16384)
4
  sts p+1,r21
5
  sts p,r20
6
  ldi r25,0
7
  subi r24,lo8(-(textP+16384))
8
  sbci r25,hi8(-(textP+16384))
9
  mov r31,r25
10
  mov r30,r24
11
  ld r24,Z
12
  ret
1
*(.progmem.*)
2
.progmem.data.textP
3
                0x00000024                textP
Beim Zugriff mit obigem Makro wird der Offset also doppelt addiert.
 
 
Davon ab ist progmem eh überflüssig, weil .rodata im Flash liegt und das 
Linker-Skript 0x4000 addiert:
1
const char text[] = "Text";
2
3
char read_rodata (uint8_t i)
4
{
5
    return text[i];
6
}
1
setp_read_rodata:
2
  ldi r25,0
3
  subi r24,lo8(-(text))
4
  sbci r25,hi8(-(text))
5
  mov r31,r25
6
  mov r30,r24
7
  ld r24,Z
8
  ret
1
.rodata         0x00004112        0x5 load address 0x00000112
2
 *(.rodata*)
3
.rodata.text    0x00004112        text

von TO (Gast)


Lesenswert?

Johann L. schrieb:
> TO schrieb:
>> In der avr-libc wird teilweise genau das gleiche gemacht, aber es
>> funktioniert. avr/pgmspace.h:
> [c]
>> #define __LPM_word_tiny__(addr)             \
...

> Dieses Makro ist nicht nur überflüssig, sondern auch falsch!

Ich habe es mir nicht im Detail angeschaut, sondern nur ein ähnliches 
Beispiel zu meinem Problem gesucht. Das Makro ist in der Form aber in 
der avr-libc mit Microchips patches enthalten:
1
$ cd /run/shm
2
$ apt source avr-libc 
3
Reading package lists... Done
4
Selected version '1:1.8.0+Atmel3.5.0-1' (stretch) for avr-libc
5
Need to get 4.480 kB of source archives.
6
Get:1 tor+http://vwakviie2ienjx6t.onion/debian stretch/main avr-libc 1:1.8.0+Atmel3.5.0-1 (dsc) [1.898 B]
7
Get:2 tor+http://vwakviie2ienjx6t.onion/debian stretch/main avr-libc 1:1.8.0+Atmel3.5.0-1 (tar) [4.472 kB]
8
Get:3 tor+http://vwakviie2ienjx6t.onion/debian stretch/main avr-libc 1:1.8.0+Atmel3.5.0-1 (diff) [6.039 B]                                                  
9
Fetched 4.480 kB in 12s (356 kB/s)                                                                                                                          
10
dpkg-source: info: extracting avr-libc in avr-libc-1.8.0+Atmel3.5.0
11
dpkg-source: info: unpacking avr-libc_1.8.0+Atmel3.5.0.orig.tar.gz
12
dpkg-source: info: applying avr-libc_1.8.0+Atmel3.5.0-1.diff.gz
13
$ cd avr-libc-1.8.0+Atmel3.5.0/
14
$ tar xf avr-libc.tar.bz2 
15
$ cd libc/
16
$ find  . -name pgmspace.h
17
./avr-libc/include/avr/pgmspace.h

Da gibt es eine interessante Stelle:
1
/*
2
Macro to read data from program memory for avr tiny parts(tiny 4/5/9/10/20/40).
3
why:
4
- LPM instruction is not available in AVR_TINY instruction set.
5
- Programs are executed starting from address 0x0000 in program memory.
6
But it must be addressed starting from 0x4000 when accessed via data memory.
7
Reference: TINY device (ATTiny 4,5,9,10,20 and 40) datasheets
8
Bug: avrtc-536
9
*/
10
#elif defined (__AVR_TINY__)
11
#define __LPM(addr)         __LPM_tiny__(addr)
12
#define __LPM_word(addr)    __LPM_word_tiny__(addr)
13
#define __LPM_dword(addr)   __LPM_dword_tiny__(addr)
14
#define __LPM_float(addr)   __LPM_float_tiny__(addr)
15
#else
16
#define __LPM(addr)         __LPM_classic__(addr)
17
#define __LPM_word(addr)    __LPM_word_classic__(addr)
18
#define __LPM_dword(addr)   __LPM_dword_classic__(addr)
19
#define __LPM_float(addr)   __LPM_float_classic__(addr)
20
#endif

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.