AVR GCC: Wie verbessere ich die Code-Optimierung?

Ich habe versucht, den folgenden C-Code zu kompilieren:

period = TCNT0L;
period |= ((unsigned int)TCNT0H<<8);

Der Assembler-Code, den ich bekomme, ist der folgende:

    period = TCNT0L;
  d2:   22 b7           in  r18, 0x32   ; 50
  d4:   30 e0           ldi r19, 0x00   ; 0
  d6:   30 93 87 00     sts 0x0087, r19
  da:   20 93 86 00     sts 0x0086, r18
    period |= ((unsigned int)TCNT0H<<8);
  de:   44 b3           in  r20, 0x14   ; 20
  e0:   94 2f           mov r25, r20
  e2:   80 e0           ldi r24, 0x00   ; 0
  e4:   82 2b           or  r24, r18
  e6:   93 2b           or  r25, r19
  e8:   90 93 87 00     sts 0x0087, r25
  ec:   80 93 86 00     sts 0x0086, r24

Also statt 4 Anweisungen werden es 11!

Ich habe versucht, die Optimierungsoptionen O1, O2, O3 und Os auszuwählen. Das Ergebnis ist dasselbe (außer dass diese O3Option diesen Code überhaupt wegoptimiert).

Ich könnte den Quellcode folgendermaßen schreiben:

period = TCNT0L | ((unsigned int)TCNT0H<<8);

Ich werde kleineren, aber immer noch nicht optimalen Code bekommen:

  de:   22 b7           in  r18, 0x32   ; 50
  e0:   34 b3           in  r19, 0x14   ; 20
  e2:   93 2f           mov r25, r19
  e4:   80 e0           ldi r24, 0x00   ; 0
  e6:   82 2b           or  r24, r18
  e8:   90 93 87 00     sts 0x0087, r25
  ec:   80 93 86 00     sts 0x0086, r24

Ich habe jedoch keine Garantie mehr, dass auf das untere Byte zuerst zugegriffen wird (dies ist eine wesentliche Voraussetzung, um das 16-Bit-Lesen korrekt zu halten). Und dennoch enthält der Code viele zusätzliche unnötige Anweisungen.

Kann ich die Compileroptionen ändern und/oder den Quellcode ändern, um ihn zu verbessern? Ich würde vermeiden, zum Assembler zu gehen.

UPDATE1:

Ich habe den von @caveman vorgeschlagenen Code ausprobiert:

((unsigned char*)(&period))[0] = TCNT0L;
((unsigned char*)(&period))[1] = TCNT0H;

Aber das Ergebnis ist auch nicht sehr gut:

    ((unsigned char*)(&period))[0] = TCNT0L;
  dc:   82 b7           in  r24, 0x32   ; 50
  de:   e6 e8           ldi r30, 0x86   ; 134
  e0:   f0 e0           ldi r31, 0x00   ; 0
  e2:   80 83           st  Z, r24
    ((unsigned char*)(&period))[1] = TCNT0H;
  e4:   84 b3           in  r24, 0x14   ; 20
  e6:   81 83           std Z+1, r24    ; 0x01
Können Sie nicht einfach Folgendes tun: uint8_t period = TCNT0 ?
@Golaž das ist komisch, aber mein Compiler akzeptiert TCNT0 nicht aus dem Regal. Muss ich es selbst deklarieren? Wenn ja - wie deklariere ich 16-Bit-Register (unter Berücksichtigung der Tatsache, dass der Prozessor 8-Bit ist)
Welche IDE verwendest du?
@Golaž Atmel Studio 6
Eine Sache ist, wenn Sie sich Sorgen machen, dass das LSB zuerst gelesen wird, machen Sie möglicherweise etwas falsch. Sagen Sie zum Beispiel an der 0x00FFStelle 0x0100, an der Sie vielleicht am Ende lesen 0x01FF.
@PeterJ 8-Bit-AVR bietet High-Bite-Lock nach der unteren Hälfte des 16-Bit-Registerlesens
@Roman ahh war mir dessen nicht bewusst.
"Anderen Code schreiben" scheint keine Antwort auf "Wie kann ich meinen Compiler besser optimieren zu lassen.

Antworten (4)

Eine Methode besteht darin, direkte Lasten auf die Hälften der Periode zu verwenden. Während dies in C kompliziert aussieht, erzeugt es normalerweise eine sehr enge Assemblierung, dh 2 Ladevorgänge und 2 Speichervorgänge.

((uint8_t*)(&period))[0] = TCNT0L;
((uint8_t*)(&period))[1] = TCNT0H;

Manchmal kann die Verwendung der Array-Mathematik Probleme verursachen, also könnten Sie Folgendes versuchen:

*((uint8_t*)(&period)) = TCNT0L;
*((uint8_t*)(&period) + 1) = TCNT0H;

Dies erzeugt tatsächlich optimalen Code. Sehen Sie sich an, wie 12 Bytes verwendet werden.

  ((unsigned char*)(&period))[0] = TCNT0L;
  dc:   82 b7           in  r24, 0x32   ; 50
  de:   e6 e8           ldi r30, 0x86   ; 134
  e0:   f0 e0           ldi r31, 0x00   ; 0
  e2:   80 83           st  Z, r24
    ((unsigned char*)(&period))[1] = TCNT0H;
  e4:   84 b3           in  r24, 0x14   ; 20
  e6:   81 83           std Z+1, r24    ; 0x01

Wenn Sie dies mit Assembler gemacht haben, scheint es wahrscheinlich besser zu sein, es so zu machen. Es sind auch 12 Bytes, also sind sie gleichwertig.

  dc:   82 b7           in  r24, 0x32   ; 50
  de:   80 93 86 00     sts 0x0086, r24
  e2:   84 b3           in  r24, 0x14   ; 20
  e4:   80 93 87 00     sts 0x0087, r24

Wenn ich "äquivalent" sage, meine ich natürlich die Codegröße. Wenn die Zeit wichtiger ist, dann müssen Sie sich die Zyklen ansehen. In diesem Fall sieht es so aus, als hätte die Assembly-Version 6 Zyklen und die Compiler-Version 8 Zyklen.

Habe deinen Code probiert. Teilweise gearbeitet. 7-Anweisung, die einem Ein-Operator-Ansatz entspricht :( (siehe meine Frage UPDATE1)
Sie können versuchen, diese mathematische Nicht-Array-Version zu verwenden. Aber ehrlich gesagt, verwenden Sie einfach die Inline-Assemblierung. Der Codegenerator scheint bei dieser gcc-Portierung ziemlich schlecht zu sein.
Versucht die zweite: gleiches Ergebnis. In Bezug auf R30/R31 - sieht so aus, als würde es versuchen, indirektes Speichern zu verwenden (da ich die indirekte Adressierungsmethode im Code verwendet habe), so dass es ziemlich gerechtfertigt aussieht.
Ich habe gerade Ihre letzte Änderung bemerkt: Mein Hauptanliegen ist die Ausführungszeit. Die Assembler-Version ist also das, was ich wirklich brauche. Kann ich also davon ausgehen, dass dieser Code nicht korrekt vom C-Compiler abgerufen werden kann (ohne Verwendung von Inline-Assembler)?
Ich glaube nicht, dass der Compiler es viel besser machen kann. Sie haben vieles ausprobiert. Aber lassen Sie mich fragen, ob Sie einen Schritt zurückgetreten sind und wirklich denken, dass sich 2 Zyklen lohnen? Wenn Ihr gesamter Code so eng ist, sollten Sie wahrscheinlich sowieso direkt Assembler verwenden.
Sieht so aus, als sollte ich zum Assembler gehen. Ich habe einen ISR, der möglichst schnell reagieren soll. Ich habe nur 1,5 us in meinem "Budget" und eine Menge Dinge zu tun (neben dem Teil des Codes, den ich in meiner ursprünglichen Frage beschrieben habe).
Gute Antwort, aber ich würde empfehlen, dass Sie einen Hinweis hinzufügen, dass diese Art der Zeigerumwandlung nicht portabel und im Allgemeinen eine schlechte Praxis ist (zumindest sollte sie durch eine Behauptung geschützt werden). Allerdings ist so etwas in der Embedded-Welt ziemlich verbreitet.

In meinem avr-gcc 5.4.0 period = TCNT1;scheint simple for attiny841 den Code wie folgt auszugeben:

    in  r24,0x2c
    in  r25,0x2d
    sts 0x0110,r25
    sts 0x010f,r24

Es scheint, dass der Compiler bereits weiß, wie auf 16-Bit-Register zugegriffen werden muss, und daher ist der obige Code sicher.

Der avr-Zweig des gcc ist im Allgemeinen nicht sehr gut, selbst bei einfachen Optimierungen wie den Beispielen in der Frage, aber ein Upgrade der Version von avr-gcc hilft oft.

Ein weiteres Problem ist, dass spätere gccs und spätere avr-libcs ​​tatsächlich den Zugriff auf TCNT0 als einzelnes 16-Bit-Register unterstützen könnten - was dem in der Frage verwendeten gcc zu fehlen scheint.

Ähhh! Ich hasse es jedes Mal, wenn ich dieses Motiv ausgestrahlt sehe! Warum verschwendet der Compiler immer ein zusätzliches Register, anstatt das Äquivalent zu tun.
@bigjosh Besonders schmerzhaft, wenn Sie nur a (oder länger) in einer ISR erhöhen möchten uint32_t. In diesen Fällen könnte der Compiler manchmal mit 0-Registern davonkommen (verwenden Sie das temporäre Register!), aber schiebt vier davon auf den Stapel und legt sie bei der Rückkehr zurück. Für diese Spezialfälle habe ich ein asm-Makro parat.

Wenn Sie bereit sind, einen Stift zu verschwenden, könnten Sie eine 1-Befehl/2-Zyklus-Erfassung des TCNT erhalten, wenn die ISR aufgerufen wird, indem Sie die Output Capture Unit verwenden.

Aufstellen

  1. Setzen Sie das Bit im DDR für den ICP-Pin, um ihn zu einem Ausgang zu machen.
  2. Stellen Sie ACIC so ein, dass der Eingangspin für den ICU-Trigger verwendet wird. Andere ICU-Bits auf Standard belassen (kein Rauschfilter, Trigger auf fallende Flanke)

Für jede Aufnahme

Im Vordergrund

  1. Löschen Sie das ICF-Bit, indem Sie eine 1 darauf schreiben.
  2. Setzen Sie das PORT-Bit für den ICP-Pin, damit er HIGH ausgibt.
  3. Fragen Sie das ICF-Bit ab, bis es zu wird 1.
  4. Lesen Sie den erfassten TNCTWert aus dem ICRRegister.
  5. Spülen. Wiederholen.

Im ISR

  1. Setzen Sie das PORT-Bit für den ICP-Pin mit dem SBI-Befehl.

Geben Sie hier die Bildbeschreibung ein

Geben Sie hier die Bildbeschreibung ein

Geben Sie hier die Bildbeschreibung ein

Wenn Sie mit Zykleneinsparungen hart werden wollen, können Sie diesen Zugriff auf TCNT auf einen einzigen Zyklus in der ISR reduzieren!

Sie können sich die Tatsache zunutze machen, dass das High-Byte des TCNT-Registers gepuffert wird, wenn das Low-Byte gelesen wird.

Geben Sie hier die Bildbeschreibung ein

Wenn Sie also ein Register (z. B. r16) für diese Aufgabe vorbelegt haben ...

register unsigned char tcnt_low_byte asm("r16");

... dann dieses Register mit dem Low-Byte TCNTder ISR so gefüllt ...

R16 = TCNTL;

... die auf den 1-Zyklus herunterkompiliert werden sollte ...

IN R16,TCNTL

TCNT...dann könnten Sie später den gesamten Schnappschuss- Wert so im Vordergrund auslesen ....

period = (TCNTH << 8)| R16;

Stellen Sie nur sicher, dass Sie lesen TCNTH, bevor Sie auf andere 16-Bit-Timer-Register zugreifen, da alle dieses Temp-Temp-Register gemeinsam nutzen.

Die gesamte Arbeit, die in der ISR geleistet wird, ist nur eine einzige, in R16, TCNTLdie 1 Zyklus ist.

Das OP hat nicht angegeben, wie er dem Vordergrundprozess signalisieren würde, dass eine ISR stattgefunden hat, aber wenn er vorab geladen periodund 0dann nach einer Änderung gesucht hat, ist zusätzliche Arbeit erforderlich ...

  1. 0in das 16-Bit-Register vorladen TEMP(Sie können dies tun, indem Sie a 0in ein beliebiges 16-Bit-Register schreiben).
  2. vorladen 0in R16.

Dann können Sie abfragen, ob die ISR passiert ist mit...

x=TCNTH
if (x || R16) {
    period=(x<<8 | R16)
    // Process new period capture here...
}
Interessante Idee. Aber wie würde er der Hauptschleife von der ISR mitteilen, dass TCNTL gelesen wurde und es Zeit ist, TCNTH zu lesen?
@JimmyB Tolle Frage, das hatte ich nicht bedacht! Vermutlich unter Verwendung des Mechanismus, den das OP verwendet hat, um zu signalisieren, dass der TCNT-Wert in seinem Beispiel erfasst wurde period(nicht gezeigt)? Antwort aktualisiert für den Fall, wenn sein Mechanismus den Zeitraum auf voreinstellen 0und dann abfragen sollte.
@JimmyB Siehst du irgendwelche Probleme? Ich denke, der Zugriff auf x+R16 muss nicht einmal atomar sein.