- Die erste Assembly-Lektion von FFmpeg erklärt auf Einstiegsniveau, warum handgeschriebenes SIMD-Assembly in der Multimedia-Verarbeitung nötig ist und welche Konventionen FFmpeg für das Schreiben von Funktionen verwendet.
- Zielgruppe sind Leser, die C-Zeiger sowie Konzepte wie Skalare, Vektoren, Addition und Multiplikation auf Oberstufenniveau kennen; die Lektion basiert auf x86 64-bit und Intel-Syntax.
- In FFmpeg werden assembly function, SIMD und vectorise nahezu gleichbedeutend verwendet; die Verarbeitung mehrerer Datenelemente auf einmal passt gut zur Bild-, Video- und Audioverarbeitung.
- Aus Performance-Gründen wird manuelles Assembly bevorzugt; intrinsics sind in der Regel 10–15 % langsamer, und es werden Fälle verglichen, in denen die automatische Vektorisierung von dav1d etwa 2x erreichte, die handgeschriebene Version dagegen bis zu 8x.
- Die erste Beispielfunktion nutzt
x86inc.asm, INIT_XMM sse2, cglobal, movu, paddb und RET, um die 16-Byte-Werte zweier uint8_t-Puffer per SIMD zu addieren und wieder im ersten Puffer zu speichern.
Ziel der Lektion und vorausgesetztes Wissen
- FFmpeg Assembly Language Lesson One behandelt die Grundlagen, wie in FFmpeg Assembly-Sprache geschrieben wird, und hilft dabei zu verstehen, was im Inneren des Computers tatsächlich passiert.
- Erforderliches Wissen:
- Die Programmiersprache C, insbesondere Zeiger
- Wer C nicht kennt, sollte The C Programming Language durcharbeiten.
- Konzepte aus der Schulmathematik wie Skalare und Vektoren, Addition und Multiplikation
Assembly-Sprache und SIMD
- Assembly-Sprache ist eine Programmiersprache, in der Code in für Menschen lesbarer Form geschrieben wird und direkt den Befehlen entspricht, die die CPU ausführt.
- Der menschenlesbare Assembly-Code wird durch einen Assembler in binäre Maschinencode(machine code)-Daten umgewandelt, die die CPU versteht.
- Der Assembly-Code von FFmpeg liegt größtenteils in Form von SIMD(Single Instruction Multiple Data) vor.
- Ein einzelner Befehl wirkt gleichzeitig auf mehrere Datenelemente.
- Das wird auch Vektorprogrammierung genannt.
- Normale skalare Programmierung verarbeitet jeweils ein Datenelement auf einmal.
- SIMD eignet sich gut für Bild-, Video- und Audioverarbeitung, bei der viele sequenziell im Speicher abgelegte Daten verarbeitet werden.
- In FFmpeg werden die folgenden Ausdrücke nahezu gleichbedeutend verwendet:
assembly function
SIMD
vectorise
- Gemeint ist, dass man eine Assembly-Funktion von Hand schreibt, um mehrere Datenelemente auf einmal zu verarbeiten.
Warum FFmpeg Assembly direkt schreibt
- Das Kernziel ist die Beschleunigung der Multimedia-Verarbeitung.
- Durch Assembly-Code sind Geschwindigkeitssteigerungen von über 10x üblich.
- Das ist wichtig, um Ruckeln bei der Videowiedergabe in Echtzeit zu reduzieren.
- Es kann den Energieverbrauch senken und die Akkulaufzeit verlängern.
- Funktionen für Video-Encoding und -Decoding werden von Endnutzern und in Rechenzentren sehr häufig verwendet, daher summieren sich selbst kleine Verbesserungen schnell.
- FFmpeg verwendet statt intrinsics handgeschriebenes Assembly.
- intrinsics sind C-ähnliche Funktionen, die Assembly-Befehlen entsprechen.
- Sie sind in der Regel 10–15 % langsamer als handgeschriebenes Assembly.
- Diese Zahl hängt vom Compiler ab, und Befürworter von intrinsics könnten ihr widersprechen.
- Es gibt auch die Ansicht, dass sie wegen der Verwendung von Hungarian Notation schwer lesbar sind.
- inline assembly kann noch in einigen älteren FFmpeg-Codebestandteilen oder in Projekten wie dem Linux Kernel vorhanden sein.
- Dabei wird Assembly nicht in einer separaten Datei, sondern direkt in C-Code geschrieben.
- In Projekten wie FFmpeg überwiegt die Ansicht, dass dies schwer lesbar ist, nicht breit genug von Compilern unterstützt wird und schwer zu warten ist.
- Die Ansicht, dass automatische Vektorisierung durch den Compiler ausreiche, soll zu Lernzwecken ignoriert werden.
- In jüngsten Tests des dav1d-Projekts zeigte automatische Vektorisierung eine Beschleunigung von etwa 2x.
- Die handgeschriebene Version konnte bis zu 8x erreichen.
Syntaxumfang und Referenzen
- Die Lektion konzentriert sich auf x86-64-bit-Assembly.
- Es wird auch amd64 genannt und läuft auch auf Intel-CPUs.
- Assembly für andere CPUs wie ARM oder RISC-V könnte künftig ergänzt werden.
- Für x86-Assembly gibt es AT&T- und Intel-Syntax.
- Die AT&T-Syntax ist älter und gilt als schwerer lesbar als die Intel-Syntax.
- Die Lektion verwendet Intel-Syntax.
- Allgemeine Bücher oder Online-Ressourcen wie Stack Overflow sind als Referenz für FFmpeg-Assembly möglicherweise nicht besonders nützlich.
- Der Grund ist, dass handgeschriebenes Assembly in Intel-Syntax verwendet wird.
- Viele Online-Ressourcen konzentrieren sich auf Betriebssystemprogrammierung, Hardwareprogrammierung oder Nicht-SIMD-Code.
- FFmpeg-Assembly ist ein spezieller Ansatz, der auf hochperformante Bildverarbeitung ausgerichtet ist.
- Die Diagramme in der zweiten Hälfte von The Art of 64-bit assembly, die SIMD-Befehle und deren Verhalten visualisieren, können hilfreich sein.
- Für Fragen wird ein Discord-Server bereitgestellt.
Grundkonzept der Register
- Register sind Bereiche innerhalb der CPU, in denen Daten verarbeitet werden.
- Die CPU führt Operationen nicht direkt auf dem Speicher aus; sie lädt Daten in Register, verarbeitet sie dort und schreibt sie anschließend wieder in den Speicher.
- In Assembly kann man Daten im Allgemeinen nicht direkt von einer Speicherposition an eine andere kopieren, sondern muss sie zuerst über ein Register führen.
General-Purpose-Register
- GPR(General Purpose Register) sind universelle Register, die Daten oder Speicheradressen enthalten können.
- Hier können sie Werte bis zu 64 bit enthalten.
- Sie können auch Zeiger enthalten.
- Operationen wie Addition, Multiplikation und Shifts sind möglich.
- Viele Assembly-Bücher behandeln Details und historische Hintergründe von GPR ausführlich.
- Im Assembly-Code von FFmpeg dienen GPR hauptsächlich als Gerüst(scaffolding); der Großteil ihrer Komplexität wird nicht benötigt oder abstrahiert.
Vektorregister und Datengrößen
- Vektorregister enthalten mehrere Datenelemente.
- Die wichtigsten Vektorregister auf x86 sind:
mm: MMX-Register, 64 bit groß, historisch und heute nicht mehr häufig verwendet
xmm: XMM-Register, 128 bit groß, breit verfügbar
ymm: YMM-Register, 256 bit groß, mit einigen Komplexitäten bei der Verwendung
zmm: ZMM-Register, 512 bit groß, nur eingeschränkt einsetzbar
- Da die meisten Berechnungen bei Videokompression und -dekompression ganzzahlbasiert sind, konzentriert sich auch die Lektion auf Integer.
- Ein einzelnes 128-bit-
xmm-Register kann wie folgt interpretiert werden:
- 16 byte zu je 8 bit
- 8 word zu je 16 bit
- 4 doubleword zu je 32 bit
- 2 quadword zu je 64 bit
- Abkürzungen werden später wichtig:
- byte: 8-bit-Daten
- word: 16-bit-Daten
- doubleword: 32-bit-Daten
- quadword: 64-bit-Daten
- double quadword: 128-bit-Daten
Die Rolle von x86inc.asm
x86inc.asm ist eine leichtgewichtige Abstraktionsschicht, die in FFmpeg, x264 und dav1d verwendet wird.
- Sie stellt verschiedene Funktionen bereit, die Assembly-Programmierern das Schreiben von Code erleichtern.
- Eine der am Anfang wichtigen Funktionen ist, GPR mit Labels wie
r0, r1 oder r2 zu versehen.
- Man muss sich die tatsächlichen Registernamen nicht merken.
- Da GPR in FFmpeg vor allem als Gerüst dienen, reduziert das den Schreibaufwand.
Einfaches skalares asm-Beispiel
mov r0q, 3
inc r0q
dec r0q
imul r0q, 5
- Die erste Zeile speichert den Immediate Value
3 als quadword im Register r0.
- Ein Immediate Value ist kein aus dem Speicher geladener Wert, sondern ein im Assembly-Code selbst gespeicherter Wert.
- In Intel-Syntax wird der Quelloperand rechts in den Zieloperanden links übertragen.
- Man kann es wie
r0q = 3 lesen.
- Die Reihenfolge ähnelt dem Verhalten von
memcpy.
- Das Suffix
q in r0q zeigt an, dass das Register als quadword verwendet wird.
- Danach geschieht Folgendes:
- Durch
inc wird der Wert zu 4.
- Durch
dec wird der Wert wieder zu 3.
- Durch
imul wird mit 5 multipliziert, sodass r0q am Ende 15 ist.
- Menschenlesbare Befehle wie
mov oder inc heißen mnemonics.
- Der Assembler wandelt sie in Maschinencode um.
- Großgeschriebenes
MOV, INC und kleingeschriebenes mov, inc sind identisch.
- In FFmpeg werden mnemonics kleingeschrieben; Großbuchstaben bleiben Makros vorbehalten.
Erstes SIMD-Funktionsbeispiel
%include "x86inc.asm"
SECTION .text
;static void add_values(uint8_t *src, const uint8_t * src2)
INIT_XMM sse2
cglobal add_values, 2, 2, 2, src, src2
movu m0, [srcq]
movu m1, [src2q]
paddb m0, m1
movu [srcq], m0
RET
- Diese Funktion addiert die Daten aus
src und src2 per SIMD und speichert das Ergebnis wieder an der Position von src.
%include "x86inc.asm" bindet Helfer, vordefinierte Namen und Makros ein, die von den Communities rund um x264, FFmpeg und dav1d entwickelt wurden.
SECTION .text bezeichnet den Abschnitt, in dem ausführbarer Code liegt.
- Konstante Daten können im Abschnitt
.data liegen.
;static void add_values(uint8_t *src, const uint8_t * src2) ist ein Kommentar, der die C-Funktionsargumente zeigt.
- In Assembly dient das Semikolon
; als Kommentar, ähnlich wie // in C.
INIT_XMM sse2 aktiviert die Verwendung von XMM-Registern und des Befehlssatzes sse2.
- Der Grund ist, dass
paddb ein sse2-Befehl ist.
cglobal add_values, 2, 2, 2, src, src2 definiert die C-Funktion add_values.
- Die Funktion hat 2 Argumente.
- Die Zahl der in der Funktion verwendeten GPR, einschließlich der Argumente, ist 2.
- Die Zahl der verwendeten XMM-Register ist 2.
- Die letzten beiden Einträge sind die Argument-Labels
src und src2.
- Älterer Code kann ohne Argument-Labels direkt GPR wie
r0 oder r1 verwenden.
Load, Packed Add, Store
movu m0, [srcq]
movu m1, [src2q]
movu ist eine Kurzform von movdqu und steht für move double quad unaligned.
- Alignment wird in späteren Lektionen behandelt; hier kann man es als Befehl verstehen, der 128 bit aus
[srcq] bewegt.
- Eckige Klammern bei
mov bedeuten, dass eine Adresse dereferenziert wird.
- Das ist ähnlich wie
*src in C.
- Dieser Vorgang ist ein load.
- Das Suffix
q bezeichnet die Zeigergröße.
- Auf 64-bit-Systemen steht es für 8 Byte, also die Größe eines C-Zeigers.
- x86asm verwendet auf 32-bit-Systemen 32 bit.
- Der tatsächliche load umfasst 128 bit.
- Vektorregister werden nicht mit dem vollständigen Namen wie
xmm0, sondern über das abstrahierte m0 referenziert.
- Das hängt mit der später behandelten Methode zusammen, denselben Code für mehrere SIMD-Registergrößen nutzbar zu machen.
paddb m0, m1
paddb addiert die byte-Elemente der beiden Register miteinander.
- Das Präfix
p steht für packed und dient dazu, Vektorbefehle von skalaren Befehlen zu unterscheiden.
- Das Suffix
b steht für Addition auf byte-Ebene.
- Wenn zwei Register mit 16 byte addiert werden, werden die entsprechenden Elemente positionsweise addiert, etwa
a+q, b+r, c+s.
movu [srcq], m0
RET
movu [srcq], m0 schreibt die Ergebnisdaten zurück an die Adresse, auf die der Zeiger srcq zeigt.
- Dieser Vorgang ist ein store.
RET ist ein Makro, das die Rückkehr aus der Funktion angibt.
- Fast alle Assembly-Funktionen in FFmpeg ändern Daten, die als Argumente übergeben wurden, statt einen Wert zurückzugeben.
- In der Aufgabe geht es anschließend darum, Funktionszeiger für verfügbare Assembly-Funktionen zu erstellen und zu verwenden.
Noch keine Kommentare.