Im Moment ist LLVM bereits zu einem sehr beliebten System geworden, mit dem viele Menschen aktiv verschiedene Compiler, Analysatoren usw. erstellen. Eine große Anzahl nützlicher Materialien zu diesem Thema wurde bereits verfasst, auch in russischer Sprache, was eine gute Nachricht ist. In den meisten Fällen liegt die Hauptverzerrung in den Artikeln jedoch im Frontend- und Middleend-LLVM. Natürlich wird bei der Beschreibung des vollständigen Schemas des LLVM-Betriebs die Maschinencodegenerierung nicht umgangen, aber im Grunde wird dieses Thema beiläufig angesprochen, insbesondere in Veröffentlichungen in russischer Sprache. Gleichzeitig verfügt LLVM über einen ziemlich flexiblen und interessanten Mechanismus zur Beschreibung von Prozessorarchitekturen. Daher wird dieses Material dem etwas vernachlässigten Dienstprogramm TableGen gewidmet, das Teil von LLVM ist.
Der Grund, warum der Compiler Informationen über die Architektur jeder der Zielplattformen benötigen muss, liegt auf der Hand. Natürlich hat jedes Prozessormodell seinen eigenen Registersatz, seine eigenen Maschinenanweisungen usw. Der Compiler muss über alle erforderlichen Informationen verfügen, um gültigen und effizienten Maschinencode generieren zu können. Der Compiler löst verschiedene plattformspezifische Aufgaben: Verteilt Register usw. Darüber hinaus führen LLVM-Backends auch Optimierungen bereits am Maschinen-IR durch, das näher an den tatsächlichen Anweisungen liegt, oder an den Assembler-Anweisungen selbst. Bei solchen Optimierungen müssen Anweisungen ersetzt und transformiert werden. Dementsprechend sollten alle Informationen über sie verfügbar sein.
Um das Problem der Beschreibung der Prozessorarchitektur zu lösen, hat LLVM ein einziges Format zur Bestimmung der für den Compiler erforderlichen Prozessoreigenschaften verwendet. Für jede unterstützte Architektur enthält eine
.td
eine Beschreibung in einer speziellen formalen Sprache. Es wird in
.inc
Dateien konvertiert, wenn der Compiler mit dem in LLVM enthaltenen Dienstprogramm TableGen erstellt wird. Die resultierenden Dateien sind zwar C-Quellen, haben jedoch höchstwahrscheinlich eine separate Erweiterung, damit diese automatisch generierten Dateien leicht unterschieden und gefiltert werden können. Die offizielle Dokumentation für TableGen ist
hier und enthält alle notwendigen Informationen, es gibt auch eine
formale Beschreibung der Sprache und eine
allgemeine Einführung .
Dies ist natürlich ein sehr umfangreiches Thema, bei dem es viele Details gibt, über die Sie einzelne Artikel schreiben können. In diesem Artikel betrachten wir einfach die grundlegenden Punkte der Beschreibung von Prozessoren, auch ohne einen Überblick über alle Funktionen.
Beschreibung der Architektur in der .td-Datei
Die in TableGen verwendete formale Beschreibungssprache weist also ähnliche Funktionen wie normale Programmiersprachen auf und ermöglicht es Ihnen, die Merkmale der Architektur in einem deklarativen Stil zu beschreiben. Und so wie ich es verstehe, wird diese Sprache auch allgemein als TableGen bezeichnet. Das heißt In diesem Artikel verwendet TableGen sowohl den Namen der formalen Sprache selbst als auch das Dienstprogramm, das die daraus resultierenden Artefakte generiert.
Moderne Prozessoren sind sehr komplexe Systeme, daher ist es nicht verwunderlich, dass ihre Beschreibung ziemlich umfangreich ist. Um die Struktur zu erstellen und die Wartung von
.td
Dateien zu vereinfachen, können sie sich daher unter Verwendung der für C-Programmierer üblichen
#include
Direktive gegenseitig
#include
. Mithilfe dieser Direktive wird immer zuerst die Datei
Target.td
, die plattformunabhängige Schnittstellen enthält, die implementiert werden müssen, um alle erforderlichen TableGen-Informationen bereitzustellen. Diese Datei enthält bereits eine
.td
Datei mit LLVM-Eigenbeschreibungen, enthält jedoch hauptsächlich Basisklassen wie
Register
,
Instruction
,
Processor
usw., von denen Sie erben müssen, um Ihre eigene Architektur für den Compiler basierend auf zu erstellen LLVM. Aus dem vorhergehenden Satz geht klar hervor, dass TableGen den Begriff Klassen hat, der allen Programmierern bekannt ist.
Im Allgemeinen hat TableGen nur zwei grundlegende Entitäten:
Klassen und
Definitionen .
Klassen
TableGen-Klassen sind wie in allen objektorientierten Programmiersprachen auch Abstraktionen, aber einfachere Entitäten.
Klassen können Parameter und Felder haben und andere Klassen erben.
Zum Beispiel wird eine der Basisklassen unten dargestellt.
Die spitzen Klammern geben die Eingabeparameter an, die den Klasseneigenschaften zugewiesen sind. In diesem Beispiel können Sie auch feststellen, dass die TableGen-Sprache statisch typisiert ist. Die Typen, die in TableGen existieren:
bit
(ein Analogon des Booleschen Typs mit den Werten 0 und 1),
int
,
string
,
code
(ein Stück Code, dies ist ein Typ, einfach weil TableGen keine Methoden und Funktionen im üblichen Sinne hat, die Codezeilen sind in
[{ ... }]
), Bits <n>, Liste <Typ> (Werte werden wie in Python und einigen anderen Programmiersprachen in eckigen Klammern [...] gesetzt),
class type
,
dag
.
Die meisten Typen sollten verstanden werden, aber wenn sie Fragen haben, werden sie alle ausführlich in der Sprachspezifikation beschrieben, die unter dem am Anfang des Artikels angegebenen Link verfügbar ist.
Vererbung wird auch durch eine ziemlich vertraute Syntax beschrieben mit
:
class X86MemOperand<string printMethod, AsmOperandClass parserMatchClass = X86MemAsmOperand> : Operand<iPTR> { let PrintMethod = printMethod; let MIOperandInfo = (ops ptr_rc, i8imm, ptr_rc_nosp, i32imm, SEGMENT_REG); let ParserMatchClass = parserMatchClass; let OperandType = "OPERAND_MEMORY"; }
In diesem Fall kann die erstellte Klasse natürlich die Werte der in der Basisklasse angegebenen Felder mit dem Schlüsselwort
let
überschreiben. Außerdem können eigene Felder hinzugefügt werden, die der Beschreibung im vorherigen Beispiel ähneln und den Feldtyp angeben.
Definitionen
Definitionen sind bereits konkrete Entitäten, Sie können sie mit den vertrauten Objekten aller Objekte vergleichen. Definitionen werden mit dem Schlüsselwort
def
definiert und können eine Klasse implementieren, Felder von Basisklassen genauso wie oben beschrieben neu definieren und auch eigene Felder haben.
def i8mem : X86MemOperand<"printbytemem", X86Mem8AsmOperand>; def X86AbsMemAsmOperand : AsmOperandClass { let Name = "AbsMem"; let SuperClasses = [X86MemAsmOperand]; }
Multiklassen
Natürlich hat eine große Anzahl von Anweisungen in Prozessoren eine ähnliche Semantik. Beispielsweise kann es einen Satz von Anweisungen mit drei Adressen geben, die die beiden Formen
“reg = reg op reg”
und
“reg = reg op imm”
annehmen. In einem Fall werden Werte aus den Registern entnommen und das Ergebnis wird ebenfalls im Register gespeichert, und in dem anderen Fall ist der zweite Operand ein konstanter Wert (unmittelbarer Operand).
Das manuelle Auflisten aller Kombinationen ist ziemlich mühsam, das Risiko eines Fehlers steigt. Natürlich können sie automatisch durch Schreiben eines einfachen Skripts generiert werden, dies ist jedoch nicht erforderlich, da in der TableGen-Sprache ein Konzept wie mehrere Klassen vorhanden ist.
multiclass ri_inst<int opc, string asmstr> { def _rr : inst<opc, !strconcat(asmstr, " $dst, $src1, $src2"), (ops GPR:$dst, GPR:$src1, GPR:$src2)>; def _ri : inst<opc, !strconcat(asmstr, " $dst, $src1, $src2"), (ops GPR:$dst, GPR:$src1, Imm:$src2)>; }
Innerhalb von Multiclasses müssen Sie alle möglichen Formen von Anweisungen mit dem Schlüsselwort
def
. Dies ist jedoch keine vollständige Form der zu generierenden Anweisungen. Gleichzeitig können Sie die Felder in ihnen neu definieren und alles tun, was in den üblichen Definitionen möglich ist. Um echte Definitionen basierend auf einer Mehrfachklasse zu erstellen, müssen Sie das Schlüsselwort
defm
.
Infolgedessen werden für jede solche Definition, die durch
defm
tatsächlich mehrere Definitionen konstruiert, die eine Kombination aus der Hauptanweisung und allen möglichen Formen sind, die in der Mehrfachklasse beschrieben sind. In diesem Beispiel werden die folgenden Anweisungen generiert:
ADD_rr
,
ADD_ri
,
SUB_rr
,
SUB_ri
,
MUL_rr
,
MUL_ri
.
Multiklassen können nicht nur Definitionen mit
def
, sondern auch verschachtelte
defm
, wodurch die Erzeugung komplexer
defm
ermöglicht wird. Ein Beispiel für die Erstellung solcher Ketten finden Sie in der offiziellen Dokumentation.
Unterziele
Eine weitere grundlegende und nützliche Sache für Prozessoren mit unterschiedlichen Variationen des Befehlssatzes ist die Unterstützung von Unterzielen in LLVM. Ein Beispiel für eine Verwendung ist die LLVM-SPARC-Implementierung, die drei Hauptversionen der SPARC-Mikroprozessorarchitektur gleichzeitig abdeckt: Version 8 (V8, 32-Bit-Architektur), Version 9 (V9, 64-Bit-Architektur) und UltraSPARC-Architektur. Der Unterschied zwischen den Architekturen ist ziemlich groß, eine unterschiedliche Anzahl von Registern unterschiedlichen Typs, eine unterstützte Bytereihenfolge usw. In solchen Fällen
XXXSubtarget
es sich, bei mehreren Konfigurationen die
XXXSubtarget
Klasse für die Architektur zu implementieren. Die Verwendung dieser Klasse in der Beschreibung führt zu den neuen Befehlszeilenoptionen
-mcpu=
und
-mattr=
.
Neben der
Subtarget
Klasse selbst ist die
Subtarget
Klasse wichtig.
class SubtargetFeature<string n, string a, string v, string d, list<SubtargetFeature> i = []> { string Name = n; string Attribute = a; string Value = v; string Desc = d; list<SubtargetFeature> Implies = i; }
In der Datei
Sparc.td
finden Sie Beispiele für die Implementierung von
SubtargetFeature
, mit denen Sie die Verfügbarkeit einer Reihe von Anweisungen für jeden einzelnen Subtyp der Architektur beschreiben können.
def FeatureV9 : SubtargetFeature<"v9", "IsV9", "true", "Enable SPARC-V9 instructions">; def FeatureV8Deprecated : SubtargetFeature<"deprecated-v8", "V8DeprecatedInsts", "true", "Enable deprecated V8 instructions in V9 mode">; def FeatureVIS : SubtargetFeature<"vis", "IsVIS", "true", "Enable UltraSPARC Visual Instruction Set extensions">;
In diesem Fall definiert
Sparc.td
trotzdem die
Proc
Klasse, mit der bestimmte Untertypen von SPARC-Prozessoren beschrieben werden, die möglicherweise die oben beschriebenen Eigenschaften aufweisen, einschließlich verschiedener Befehlssätze.
class Proc<string Name, list<SubtargetFeature> Features> : Processor<Name, NoItineraries, Features>; def : Proc<"generic", []>; def : Proc<"v8", []>; def : Proc<"supersparc", []>; def : Proc<"sparclite", []>; def : Proc<"f934", []>; def : Proc<"hypersparc", []>; def : Proc<"sparclite86x", []>; def : Proc<"sparclet", []>; def : Proc<"tsc701", []>; def : Proc<"v9", [FeatureV9]>; def : Proc<"ultrasparc", [FeatureV9, FeatureV8Deprecated]>; def : Proc<"ultrasparc3", [FeatureV9, FeatureV8Deprecated]>; def : Proc<"ultrasparc3-vis", [FeatureV9, FeatureV8Deprecated, FeatureVIS]>;
Beziehung zwischen den Eigenschaften von Anweisungen in TableGen und dem LLVM-Backend-Code
Mit den Eigenschaften von Klassen und Definitionen können Sie Architekturmerkmale korrekt generieren und festlegen, es besteht jedoch kein direkter Zugriff auf sie über den LLVM-Backend-Quellcode. Manchmal möchten Sie jedoch in der Lage sein, einige plattformspezifische Eigenschaften von Anweisungen direkt im Compiler-Code abzurufen.
TSFlags
Zu diesem
TSFlags
verfügt die
Instruction
Basisklasse über ein spezielles
TSFlags
Feld mit einer Größe von 64 Bit, das von TableGen in ein Feld von C ++ - Objekten der Klasse
MCInstrDesc
, das auf der Grundlage der aus der TableGen-Beschreibung empfangenen Daten generiert wird. Sie können eine beliebige Anzahl von Bits angeben, die Sie zum Speichern von Informationen benötigen. Dies kann beispielsweise ein boolescher Wert sein, um anzuzeigen, dass wir eine skalare ALU verwenden.
let TSFlags{0} = SALU;
Oder wir können die Art der Anweisung speichern. Dann brauchen wir natürlich mehr als ein Bit.
Dadurch wird es möglich, diese Eigenschaften aus der Anweisung im Backend-Code abzurufen.
bool isSALU = MI.getDesc().TSFlags & SIInstrFlags::SALU;
Wenn die Eigenschaft komplexer ist, können Sie sie mit dem in TableGen beschriebenen Wert vergleichen, der der automatisch generierten Aufzählung hinzugefügt wird.
(Desc.TSFlags & X86II::FormMask) == X86II::MRMSrcMem
Funktionsprädikate
Außerdem können Funktionsprädikate verwendet werden, um die erforderlichen Informationen zu Anweisungen zu erhalten. Mit ihrer Hilfe können Sie TableGen zeigen, dass Sie eine Funktion generieren müssen, die entsprechend im Backend-Code verfügbar ist. Die Basisklasse, mit der Sie eine solche Funktionsdefinition erstellen können, ist unten dargestellt.
Verwendungsbeispiele finden Sie leicht im Backend für X86. Es gibt also eine eigene Zwischenklasse, mit deren Hilfe bereits die notwendigen Definitionen von Funktionen erstellt werden.
Daher können Sie die
isThreeOperandsLEA
Methode in C ++ - Code verwenden.
if (!(TII->isThreeOperandsLEA(MI) || hasInefficientLEABaseReg(Base, Index)) || !TII->isSafeToClobberEFLAGS(MBB, MI) || Segment.getReg() != X86::NoRegister) return;
Hier ist TII die
getInstrInfo()
, die mit der Methode
getInstrInfo()
vom
MCSubtargetInfo
für die gewünschte Architektur abgerufen werden kann.
Transformation von Anweisungen während Optimierungen. Anweisungszuordnung
Während einer großen Anzahl von Optimierungen, die in den späteren Phasen der Kompilierung durchgeführt werden, entsteht häufig die Aufgabe, alle oder nur einen Teil der Anweisungen eines Formulars in Anweisungen eines anderen Formulars zu konvertieren. Angesichts der Anwendung der eingangs beschriebenen Multiklassen können wir eine große Anzahl von Anweisungen mit ähnlicher Semantik und ähnlichen Eigenschaften haben. Im Code könnten diese Transformationen natürlich in Form großer
switch-case
Konstruktionen geschrieben werden, die für jeden Befehl die entsprechende Transformation zerquetschen. Teilweise können diese riesigen Konstruktionen mit Hilfe von Makros reduziert werden, die nach bekannten Regeln den notwendigen Namen der Anweisung bilden würden. Trotzdem ist dieser Ansatz sehr unpraktisch und schwierig zu pflegen, da alle Befehlsnamen explizit aufgelistet sind. Das Hinzufügen einer neuen Anweisung kann sehr leicht zu einem Fehler führen, weil Sie müssen daran denken, es allen relevanten Conversions hinzuzufügen. LLVM wurde mit diesem Ansatz gequält und entwickelte einen speziellen Mechanismus, um eine Befehlsform effizient in ein anderes Befehls-
Instruction Mapping
umzuwandeln.
Die Idee ist sehr einfach, es ist notwendig, mögliche Modelle zum Transformieren von Anweisungen direkt in TableGen zu beschreiben. Daher gibt es in LLVM TableGen eine Basisklasse zur Beschreibung solcher Modelle.
class InstrMapping {
Schauen wir uns ein Beispiel an, das in der Dokumentation angegeben ist. Die Beispiele, die im Quellcode zu finden sind, sind jetzt noch einfacher, da nur zwei Spalten in der abschließenden Tabelle enthalten sind. Im Backend-Code finden Sie die Konvertierung alter Formulare in neue Anweisungsformen, dsp-Anweisungen in mmdsp usw., die mithilfe der Anweisungszuordnung beschrieben werden. Tatsächlich ist dieser Mechanismus bisher nicht so weit verbreitet, einfach weil die meisten Backends vor dem Erscheinen erstellt wurden. Damit er funktioniert, müssen Sie immer noch die richtigen Eigenschaften für die Anweisungen festlegen. Daher ist der Wechsel zu diesem Mechanismus nicht immer einfach. Möglicherweise benötigen Sie einige Refactoring.
Also zum Beispiel. Angenommen, wir haben Formen von Anweisungen ohne Prädikate und Anweisungen, bei denen das Prädikat wahr bzw. falsch ist. Wir beschreiben sie mit Hilfe einer Multiklasse und einer speziellen Klasse, die wir nur als Filter verwenden werden. Eine vereinfachte Beschreibung ohne Parameter und viele Eigenschaften kann ungefähr so aussehen.
class PredRel; multiclass MyInstruction<string name> { let BaseOpcode = name in { def : PredRel { let PredSense = ""; } def _pt: PredRel { let PredSense = "true"; } def _pf: PredRel { let PredSense = "false"; } } } defm ADD: MyInstruction<”ADD”>; defm SUB: MyIntruction<”SUB”>; defm MUL: MyInstruction<”MUL”>; …
In diesem Beispiel wird übrigens auch gezeigt, wie eine Eigenschaft für mehrere Definitionen gleichzeitig mit dem Konstrukt
let … in
überschrieben wird. Infolgedessen verfügen wir über zahlreiche Anweisungen, in denen der Basisname und die Eigenschaft gespeichert sind, die das Formular eindeutig beschreiben. Anschließend können Sie ein Transformationsmodell erstellen.
def getPredOpcode : InstrMapping {
Infolgedessen wird aus dieser Beschreibung die folgende Tabelle generiert.
In der
.inc
Datei wird eine Funktion generiert
int getPredOpcode(uint16_t Opcode, enum PredSense inPredSense)
Dieser akzeptiert dementsprechend einen Anweisungscode für die Konvertierung und den Wert der automatisch generierten PredSense-Aufzählung, die alle möglichen Werte in den Spalten enthält. Die Implementierung dieser Funktion ist sehr einfach, weil es gibt das gewünschte Array-Element für die Anweisung zurück, die für uns von Interesse ist.
Und im Backend-Code reicht es aus, anstatt einen
switch-case
schreiben, einfach die generierte Funktion aufzurufen, die den Code der konvertierten Anweisung zurückgibt. Eine einfache Lösung, bei der neue Anweisungen hinzugefügt werden, erfordert keine zusätzlichen Maßnahmen.
.inc
generierte Artefakte ( .inc
Dateien)
Die gesamte Interaktion zwischen der TableGen-Beschreibung und dem LLVM-Backend-Code wird durch die generierten
.inc
Dateien sichergestellt, die den C-Code enthalten. Um ein vollständiges Bild zu erhalten, schauen wir uns an, was genau das ist.
Nach jedem Build befinden sich für jede Architektur mehrere
.inc
Dateien im Build-Verzeichnis, in denen jeweils separate Informationen zur Architektur gespeichert sind.
<TargetName>GenInstrInfo.inc
, ,
<TargetName>GenRegisterInfo.inc
, ,
<TargetName>GenAsmMatcher.inc
<TargetName>GenAsmWriter.inc
..
? , , .
<TargetName>GenInstrInfo.inc
.
namespace , , .
namespace X86 { enum { PHI = 0, … ADD16i16 = 287, ADD16mi = 288, ADD16mi8 = 289, ADD16mr = 290, ADD16ri = 291, ADD16ri8 = 292, ADD16rm = 293, ADD16rr = 294, ADD16rr_REV = 295, … }
Als nächstes folgt ein Array, das die Eigenschaften der Anweisungen beschreibt const MCInstrDesc X86Insts[]
. Die folgenden Arrays enthalten Informationen zu Anweisungsnamen usw. Grundsätzlich werden alle Informationen in Übertragungen und Arrays gespeichert.Es gibt auch Funktionen, die mit Prädikaten beschrieben wurden. Basierend auf der Definition der Funktionsprädikate, die im vorherigen Abschnitt erläutert wurde, wird die folgende Funktion generiert. bool X86InstrInfo::isThreeOperandsLEA(const MachineInstr &MI) { switch(MI.getOpcode()) { case X86::LEA32r: case X86::LEA64r: case X86::LEA64_32r: case X86::LEA16r: return ( MI.getOperand(1).isReg() && MI.getOperand(1).getReg() != 0 && MI.getOperand(3).isReg() && MI.getOperand(3).getReg() != 0 && ( ( MI.getOperand(4).isImm() && MI.getOperand(4).getImm() != 0 ) || (MI.getOperand(4).isGlobal()) ) ); default: return false; }
Die generierten Dateien und Strukturen enthalten jedoch Daten. In X86GenSubtargetInfo.inc
finden Sie ein Beispiel für die Struktur, die im Backend-Code verwendet werden sollte, um Informationen über die Architektur zu erhalten. Im vorherigen Abschnitt stellte sich heraus, dass es sich um TTI handelt. struct X86GenMCSubtargetInfo : public MCSubtargetInfo { X86GenMCSubtargetInfo(const Triple &TT, StringRef CPU, StringRef FS, ArrayRef<SubtargetFeatureKV> PF, ArrayRef<SubtargetSubTypeKV> PD, const MCWriteProcResEntry *WPR, const MCWriteLatencyEntry *WL, const MCReadAdvanceEntry *RA, const InstrStage *IS, const unsigned *OC, const unsigned *FP) : MCSubtargetInfo(TT, CPU, FS, PF, PD, WPR, WL, RA, IS, OC, FP) { } unsigned resolveVariantSchedClass(unsigned SchedClass, const MCInst *MI, unsigned CPUID) const override { return X86_MC::resolveVariantSchedClassImpl(SchedClass, MI, CPUID); } };
Bei Verwendung Subtarget
zur Beschreibung verschiedener Konfigurationen XXXGenSubtarget.inc
wird eine Aufzählung mit den beschriebenen Eigenschaften unter Verwendung von SubtargetFeature
Arrays mit konstanten Werten erstellt, um die Eigenschaften und Untertypen der CPU anzugeben, und es wird eine Funktion generiert ParseSubtargetFeatures
, die die Zeichenfolge mit dem Optionssatz verarbeitet Subtarget
. Darüber hinaus sollte die Implementierung der Methode XXXSubtarget
im Backend-Code dem folgenden Pseudocode entsprechen, in dem diese Funktion verwendet werden muss: XXXSubtarget::XXXSubtarget(const Module &M, const std::string &FS) {
,
.inc
, , . , . .
Fazit
TableGen LLVM LLVM . , .