"Finden Sie einen Grund für alles und Sie werden viel verstehen"
Vielleicht erinnern sich meine regelmäßigen Leser (nun, es kann nicht sein, dass sie es nicht waren) daran, dass ich in meinem Beitrag verwirrt war, dass das Attribut ohne Vorzeichen zur Beschreibung der Register externer Geräte verwendet wurde. In den Kommentaren wurde vorgeschlagen, dies zu tun, um undefiniertes Verhalten während der Schichten zu vermeiden, und ich stimmte zu. Wie ich kürzlich herausgefunden habe, gibt es einen weiteren Grund für diese Verwendung des Attributs und es kann nicht nur auf Register, sondern auch auf gewöhnliche Variablen angewendet werden.
Also fangen wir an.
Zunächst eine kleine Einführung in EisenAls Zielplattform betrachten wir einen 8-Bit-MK ohne Batterie (dies ist ein erbärmlicher Versuch, den kompromittierten Namen AVR zu verbergen), der die folgenden Hardware-implementierten Befehle enthält:
lsl / lsr logische Links- / Rechtsverschiebung, Low / High-Bit wird gelöscht;
rol / ror zyklische Links- / Rechtsverschiebung durch Übertragung (Verschiebung um 9 Bit);
Bei einer arithmetischen Verschiebung nach rechts wird das höchstwertige (vorzeichenbehaftete) Bit gespeichert (wir achten darauf, dass eine solche Verschiebung nach links grundsätzlich nicht möglich ist).
Alle diese Befehle werden auf dem Byte-Operanden ausgeführt und bilden die Grundlage für die Implementierung aller anderen möglichen Verschiebungen. Beispielsweise wird eine Wortverschiebung (2 Bytes rh, rl) mit einem Vorzeichen um 1 Stelle nach rechts durch die folgende Sequenz implementiert:
asr rh; ror rl;
Betrachten Sie ein einfaches Codebeispiel und den entsprechenden Assembler-Code für MK mit dem AVR-Befehlssystem, wie immer auf godbolt.org erhalten. (impliziert, dass die Optimierung aktiviert ist und sich die Variable im r24-Register befindet)
int8_t byte; byte = byte << 1;
clr r25 sbrc r24,7 com r25 lsl r24 rol r25
und sehen, dass die Operation fünf Teams dauert?
Hinweis: Wenn Ihnen jemand in den Kommentaren sagt, wie Sie dieses Fragment (und die nachfolgenden) in zwei Spalten anordnen sollen, bin ich Ihnen dankbar.
Aus dem Assembler-Code ist ersichtlich, dass die Byte-Variable in den ersten drei Befehlen zu einem Integer-Typ (16-Bit) erweitert wird und in den nächsten beiden die Doppelbyte-Nummer tatsächlich verschoben wird - gelinde gesagt ist es irgendwie seltsam.
Nach rechts zu schalten ist nicht besser
byte = byte >> 1; clr r25 sbrc r24,7 com r25 asr r25 ror r24
- die gleichen fünf Teams. In der Zwischenzeit ist es offensichtlich, dass Sie zum Ausführen der letzten Operation einen einzigen Befehl benötigen
sr r24
und für die erste Operation nicht mehr. Ich habe wiederholt festgestellt, dass der Compiler derzeit Assembler-Code erstellt, der nicht schlechter ist als ein Programmierer (obwohl es sich um ein ARM-Befehlssystem handelt), insbesondere wenn Sie ihm ein wenig helfen und plötzlich so ein Mist. Versuchen Sie jedoch, dem Compiler zu helfen, den richtigen Code zu erstellen. Möglicherweise müssen Sie die Typen in einer Schichtoperation mischen und versuchen
byte = byte >> (int8_t) 1;
- hat nicht geholfen, vom Wort "vollständig", aber die Option
byte=(uint8_t) byte >> 1;
ergibt ein etwas besseres Ergebnis
ldi r25,lo8(0) asr r25 ror r24
- drei Teams, da die Erweiterung auf das Ganze jetzt ein Team belegt - es ist besser, wenn auch nicht perfekt, dasselbe Bild für
byte=(uint8_t) byte << 1;
- drei Teams. Nun, um keine zusätzlichen Casts zu schreiben, machen wir die Variable selbst ohne Vorzeichen
uint8_t byteu;
und BINGO - Assembler Code erfüllt unsere Erwartungen voll und ganz
byteu = byteu << 1; lsr r24
Es ist seltsam, wie es scheint, was für ein Unterschied, den richtigen Typ einer Variablen sofort anzugeben oder sie direkt zu einer Operation zu bringen - aber es stellt sich heraus, dass es einen Unterschied gibt.
Weitere Studien haben gezeigt, dass der Assembler-Code den Variablentyp berücksichtigt, dem das Ergebnis zugeordnet ist, da
byteu = byte << 1;
funktioniert gut und erzeugt minimalen Code und die Option
byte = byteu << 1;
kann nicht ohne drei Teams auskommen.
Sicherlich wird ein solches Verhalten im Standard der Sprache beschrieben, frage ich diejenigen, die es im Kommentar wissen, aber ich werde noch einmal stolz erklären, dass "der Tschuktschen kein Leser ist" und ich werde die Geschichte fortsetzen.
Eine solche Technik hat also nicht dazu beigetragen, nach rechts zu wechseln - wie zuvor gab es 3 Teams (naja, das sind nicht 5, wie bei der Zeichenversion), und ich konnte das Ergebnis in keiner Weise verbessern.
In jedem Fall sehen wir jedoch, dass Schichtoperationen mit einer vorzeichenlosen Nummer schneller ausgeführt werden als mit seinem Gegner. Wenn wir also das höherwertige Bit einer Zahl nicht als Vorzeichen behandeln wollen (und im Fall von Registern ist dies normalerweise der Fall), müssen wir definitiv das vorzeichenlose Attribut hinzufügen, was wir in Zukunft tun werden.
Es stellt sich heraus, dass bei Verschiebungen im Allgemeinen alles äußerst interessant ist. Lassen Sie uns die Anzahl der Positionen erhöhen, wenn Sie nach links verschieben und die Ergebnisse betrachten: << 1 dauert 1 Taktzyklus, << 2 - 2, << 3 - 3, 4 - 2 unerwartet hat der Compiler eine knifflige Optimierung angewendet
swap r24 andi r24,lo8(-16)
Dabei tauscht der Befehl s
wap zwei Halbbytes in einem Byte aus. Basierend auf der letzten Optimierung << 5 - 3, << 6 - 4, << 7 - 3 gibt es erneut unerwartet eine weitere Optimierung
ror r24 clr r24 ror r24
Das Übertragungsbit wird verwendet, << 8 - 0 misst, da es sich gerade als 0 herausstellt, macht es keinen Sinn, weiter zu suchen.
Übrigens, hier ist eine interessante Aufgabe für Sie - für welche Mindestzeit können Sie eine Operation durchführen
uint16_t byteu; byteu = byteu << 4;
das übersetzt 0x1234 zu 0x2340. Die naheliegende Lösung besteht darin, einige Befehle viermal auszuführen
lsl rl rol rh
führt zu 4 * 2 = 8 Maßnahmen, ich habe mir schnell eine Option ausgedacht
swap rl ; 1243 swap rh ; 2143 andi rh,0xf0 ; 2043 mov tmp,rl andi tmp,0x0f or rh,tmp ; 2343 andi rl,0xf0 ; 2340
Das erfordert 7 Maßnahmen und ein Zwischenregister. Der Compiler generiert also einen Code mit 6 Befehlen und keinen Zwischenregistern - cool, ja.
Ich verstecke diesen Code unter dem Spoiler - versuche selbst eine Lösung zu finden.Hinweis: Im MK-Befehlssatz gibt es einen EXKLUSIVEN ODER-Befehl oder einen GESAMTBETRAG ZWEI
oderHier ist es, dieser wundervolle Code swap rl ; 1243 swap rh ; 2143 andi rh,0xf0 ; 2043 eor rh,rl ; 6343 andi r2l,0xf0 ; 6340 eor rh,rl ; 2340
Ich habe nur ästhetisches Vergnügen an diesem Fragment.
Normalerweise verschwindet bei 16-Bit-Zahlen der Unterschied zwischen dem Code für vorzeichenbehaftete und vorzeichenlose Zahlen, wenn er nach links verschoben wird. Das ist seltsam.
Kehren wir zu unseren Bytes zurück und bewegen uns nach rechts. Wie wir uns erinnern, haben wir für ein vorzeichenbehaftetes Byte 5 Taktzyklen, für ein vorzeichenloses Byte - 3 und diese Zeit kann nicht reduziert werden. Oder trotzdem, Sie können - ja, Sie können, aber es ist ein sehr seltsamer Weg (GCC mit aktivierten Optimierungen - "dies ist ein sehr seltsamer Ort"), nämlich
byteu = (byteu >> 1) & 0x7F;
Dies erzeugt genau einen Befehl für beide Varianten des Zeichens. Geeignet und optional
byteu = (byteu & 0xFE) >> 1;
Aber nur für eine vorzeichenlose Nummer, mit einer vorzeichenbehafteten, wird alles noch deprimierender - 7 Maßnahmen, daher untersuchen wir weiterhin nur die erste Option.
Ich kann nicht sagen, dass ich verstehe, was passiert, da es offensichtlich ist, dass eine logische Multiplikation (&) mit einer solchen Konstante nach einer solchen Verschiebung keinen Sinn ergibt (und dies auch nicht tut), aber das Vorhandensein der & -Operation den Code der Verschiebung selbst beeinflusst. "Sie sehen den Gopher - nein - und ich sehe nicht, aber er ist."
Verschiebungen um 2 usw. haben gezeigt, dass es wichtig ist, das Vorzeichenbit auszuzahlen, aber die Zahl ist anfangs ohne Vorzeichen. Im Allgemeinen wird etwas Müll erhalten. „Aber es funktioniert“ ist das einzige, was dazu gesagt werden kann.
Man kann jedoch mit Sicherheit sagen, dass Sie durch die Interpretation des Inhalts von Registern und des Speichers als vorzeichenlose Zahlen eine Reihe von Operationen (z. B. Verschieben oder Erweitern eines Werts) schneller ausführen und einen kompakteren Code generieren können. Dies kann daher dringend empfohlen werden Das Schreiben von Programmen für MK ist keine Voraussetzung, sofern nicht anders angegeben (Interpretation als Zahl ist bekannt).