Saat ini, LLVM telah menjadi sistem yang sangat populer, yang banyak orang gunakan secara aktif untuk membuat berbagai kompiler, analisa, dll. Sejumlah besar bahan berguna tentang topik ini telah ditulis, termasuk dalam bahasa Rusia, yang merupakan berita baik. Namun, dalam kebanyakan kasus, bias utama dalam artikel dibuat di LLVM frontend dan midend. Tentu saja, ketika menggambarkan skema penuh operasi LLVM, pembuatan kode mesin tidak dilewati, tetapi pada dasarnya topik ini disentuh dengan santai, terutama dalam publikasi di Rusia. Pada saat yang sama, LLVM memiliki mekanisme yang agak fleksibel dan menarik untuk menggambarkan arsitektur prosesor. Oleh karena itu, materi ini akan dikhususkan untuk TableGen utilitas yang agak terabaikan, yang merupakan bagian dari LLVM.
Alasan kompiler perlu memiliki informasi tentang arsitektur masing-masing platform target cukup jelas. Secara alami, setiap model prosesor memiliki set register sendiri, instruksi mesinnya sendiri, dll. Dan kompiler perlu memiliki semua informasi yang diperlukan tentang mereka agar dapat menghasilkan kode mesin yang valid dan efisien. Kompiler menyelesaikan berbagai tugas khusus platform: mendistribusikan register, dll. Selain itu, backend LLVM juga melakukan optimasi pada mesin IR, yang lebih dekat dengan instruksi aktual, atau pada instruksi assembler sendiri. Dalam optimisasi semacam itu, instruksi perlu diganti dan diubah, sehingga semua informasi tentangnya harus tersedia.
Untuk memecahkan masalah menggambarkan arsitektur prosesor, LLVM mengadopsi format tunggal untuk menentukan properti prosesor yang diperlukan untuk kompiler. Untuk setiap arsitektur yang didukung,
.td
berisi deskripsi dalam bahasa formal khusus. Itu dikonversi ke file
.inc
ketika membangun kompiler menggunakan utilitas TableGen yang disertakan dengan LLVM. File yang dihasilkan, pada kenyataannya, adalah sumber C, tetapi kemungkinan besar memiliki ekstensi yang terpisah, hanya agar file-file yang dihasilkan secara otomatis ini dapat dengan mudah dibedakan dan difilter. Dokumentasi resmi untuk TableGen ada di
sini dan memberikan semua informasi yang diperlukan, ada juga
deskripsi resmi bahasa dan
pengantar umum .
Tentu saja, ini adalah topik yang sangat luas, di mana ada banyak detail tentang mana Anda dapat menulis artikel individual. Dalam artikel ini, kami hanya mempertimbangkan poin dasar dari deskripsi prosesor bahkan tanpa tinjauan umum dari semua fitur.
Deskripsi arsitektur dalam file .td
Jadi, bahasa deskripsi formal yang digunakan dalam TableGen memiliki fitur yang mirip dengan bahasa pemrograman biasa dan memungkinkan Anda untuk menggambarkan karakteristik arsitektur dalam gaya deklaratif. Dan seperti yang saya pahami, bahasa ini juga biasa disebut TableGen. Yaitu Dalam artikel ini, TableGen menggunakan nama bahasa formal itu sendiri dan utilitas yang menghasilkan artefak yang dihasilkan dari itu.
Prosesor modern adalah sistem yang sangat kompleks, sehingga tidak mengherankan bahwa deskripsi mereka cukup banyak. Oleh karena itu, untuk membuat struktur dan menyederhanakan pemeliharaan file
.td
dapat menyertakan satu sama lain menggunakan arahan
#include
biasa untuk programmer C. Dengan bantuan arahan ini, file
Target.td
selalu disertakan pertama kali, yang berisi antarmuka platform independen yang harus diimplementasikan untuk memberikan semua informasi TableGen yang diperlukan. File ini sudah termasuk file
.td
dengan deskripsi intrinsik LLVM, tetapi dengan sendirinya sebagian besar berisi kelas dasar, seperti
Register
,
Instruction
,
Processor
, dll., Dari mana Anda perlu mewarisi untuk membuat arsitektur Anda sendiri untuk kompiler berdasarkan LLVM. Dari kalimat sebelumnya, jelas bahwa TableGen memiliki gagasan tentang kelas yang dikenal oleh semua programmer.
Secara umum, TableGen hanya memiliki dua entitas dasar:
kelas dan
definisi .
Kelas
Kelas TableGen juga abstraksi, seperti dalam semua bahasa pemrograman berorientasi objek, tetapi mereka adalah entitas yang lebih sederhana.
Kelas dapat memiliki parameter dan bidang, dan mereka juga bisa mewarisi kelas lain.
Sebagai contoh, salah satu kelas dasar disajikan di bawah ini.
Kurung sudut menunjukkan parameter input yang ditugaskan ke properti kelas. Dari contoh ini, Anda juga dapat melihat bahwa bahasa TableGen diketik secara statis. Tipe-tipe yang ada di TableGen:
bit
(analog dari tipe Boolean dengan nilai 0 dan 1),
int
,
string
,
code
(sepotong kode, ini adalah tipe, hanya karena TableGen tidak memiliki metode dan fungsi dalam arti biasa, baris kode ditulis dalam
[{ ... }]
), bit <n>, daftar <type> (nilai-nilai diatur menggunakan tanda kurung siku [...] seperti dalam Python dan beberapa bahasa pemrograman lain),
class type
,
dag
.
Sebagian besar jenis harus dipahami, tetapi jika mereka memiliki pertanyaan, mereka semua dijelaskan secara rinci dalam spesifikasi bahasa, tersedia di tautan yang diberikan di awal artikel.
Warisan juga dijelaskan oleh sintaks yang cukup akrab dengan
:
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"; }
Dalam hal ini, kelas yang dibuat, tentu saja, dapat menimpa nilai bidang yang ditentukan dalam kelas dasar menggunakan kata kunci
let
. Dan itu dapat menambahkan bidangnya sendiri mirip dengan deskripsi yang diberikan dalam contoh sebelumnya, menunjukkan jenis bidang.
Definisi
Definisi sudah entitas konkret, Anda dapat membandingkannya dengan yang akrab dengan semua objek. Definisi didefinisikan menggunakan kata kunci
def
dan dapat mengimplementasikan kelas, mendefinisikan kembali bidang kelas dasar dengan cara yang persis sama seperti yang dijelaskan di atas, dan juga memiliki bidangnya sendiri.
def i8mem : X86MemOperand<"printbytemem", X86Mem8AsmOperand>; def X86AbsMemAsmOperand : AsmOperandClass { let Name = "AbsMem"; let SuperClasses = [X86MemAsmOperand]; }
Multiclasses
Secara alami, sejumlah besar instruksi dalam prosesor memiliki semantik yang serupa. Misalnya, mungkin ada satu set instruksi tiga alamat yang mengambil dua bentuk
βreg = reg op regβ
dan
βreg = reg op immβ
. Dalam satu kasus, nilai diambil dari register dan hasilnya juga disimpan dalam register, dan dalam kasus lain, operan kedua adalah nilai konstan (operan tidak langsung).
Mendaftar semua kombinasi secara manual agak membosankan, risiko membuat kesalahan meningkat. Tentu saja, mereka dapat dihasilkan secara otomatis dengan menulis skrip sederhana, tetapi ini tidak perlu, karena konsep seperti multiclasses ada dalam bahasa TableGen.
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)>; }
Di dalam multiclasses, Anda perlu menjelaskan semua bentuk instruksi yang mungkin menggunakan kata kunci
def
. Tapi ini bukan bentuk instruksi yang lengkap untuk dihasilkan. Pada saat yang sama, Anda dapat mendefinisikan ulang bidang di dalamnya dan melakukan segala sesuatu yang mungkin dalam definisi yang biasa. Untuk membuat definisi nyata berdasarkan multiclass, Anda perlu menggunakan kata kunci
defm
.
Dan sebagai hasilnya, untuk setiap definisi yang diberikan melalui
defm
pada kenyataannya, beberapa definisi akan dibangun yang merupakan kombinasi dari instruksi utama dan semua bentuk yang mungkin dijelaskan dalam multiclass. Sebagai hasilnya, instruksi berikut akan dihasilkan dalam contoh ini:
ADD_rr
,
ADD_ri
,
SUB_rr
,
SUB_ri
,
MUL_rr
,
MUL_ri
.
Multiclasses dapat berisi tidak hanya definisi dengan
def
, tetapi juga
defm
bersarang, sehingga memungkinkan pembentukan bentuk instruksi yang kompleks. Contoh yang menggambarkan pembuatan rantai semacam itu dapat ditemukan dalam dokumentasi resmi.
Subtarget
Hal dasar dan berguna lainnya untuk prosesor yang memiliki variasi set instruksi yang berbeda adalah dukungan subtarget dalam LLVM. Contoh penggunaan adalah implementasi LLVM SPARC, yang mencakup tiga versi utama arsitektur mikroprosesor SPARC sekaligus: Versi 8 (V8, arsitektur 32-bit), Versi 9 (V9, arsitektur 64-bit) dan arsitektur UltraSPARC. Perbedaan antara arsitektur cukup besar, jumlah register yang berbeda dari jenis yang berbeda, urutan byte yang didukung, dll. Dalam kasus seperti itu, jika ada beberapa konfigurasi, ada baiknya menerapkan kelas
XXXSubtarget
untuk arsitektur. Menggunakan kelas ini dalam deskripsi akan menghasilkan opsi baris perintah baru
-mcpu=
dan
-mattr=
.
Selain kelas
Subtarget
itu sendiri, kelas
Subtarget
penting.
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; }
Dalam file
Sparc.td
, Anda dapat menemukan contoh implementasi
SubtargetFeature
, yang memungkinkan Anda untuk menggambarkan ketersediaan serangkaian instruksi untuk setiap subtipe individu arsitektur.
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">;
Dalam kasus ini,
Sparc.td
masih mendefinisikan kelas
Proc
, yang digunakan untuk menggambarkan subtipe spesifik dari prosesor SPARC, yang mungkin memiliki sifat-sifat yang dijelaskan di atas, termasuk set instruksi yang berbeda.
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]>;
Hubungan antara sifat-sifat instruksi di TableGen dan kode backend LLVM
Properti kelas dan definisi memungkinkan Anda untuk menghasilkan dan mengatur fitur arsitektur dengan benar, tetapi tidak ada akses langsung ke mereka dari kode sumber backend LLVM. Namun, kadang-kadang Anda ingin mendapatkan beberapa properti khusus platform secara langsung dalam kode kompiler.
TSFlags
Untuk melakukan ini, kelas dasar
Instruction
memiliki bidang
TSFlags
khusus,
TSFlags
64 bit, yang dikonversi oleh TableGen ke bidang objek C ++ kelas
MCInstrDesc
, yang dihasilkan berdasarkan data yang diterima dari deskripsi TableGen. Anda dapat menentukan jumlah bit yang Anda perlukan untuk menyimpan informasi. Ini mungkin beberapa nilai Boolean, misalnya, untuk menunjukkan bahwa kami menggunakan ALU skalar.
let TSFlags{0} = SALU;
Atau kita bisa menyimpan jenis instruksinya. Maka kita perlu, tentu saja, lebih dari satu bit.
Akibatnya, menjadi mungkin untuk mendapatkan properti ini dari instruksi di kode backend.
bool isSALU = MI.getDesc().TSFlags & SIInstrFlags::SALU;
Jika properti lebih kompleks, maka Anda dapat membandingkannya dengan nilai yang dijelaskan dalam TableGen, yang akan ditambahkan ke enumerasi yang dibuat secara otomatis.
(Desc.TSFlags & X86II::FormMask) == X86II::MRMSrcMem
Predikat fungsi
Juga, predikat fungsi dapat digunakan untuk mendapatkan informasi yang diperlukan tentang instruksi. Dengan bantuan mereka, Anda bisa menunjukkan kepada TableGen bahwa Anda perlu membuat fungsi yang sesuai akan tersedia dalam kode backend. Kelas dasar yang dengannya Anda dapat membuat definisi fungsi disajikan di bawah ini.
Anda dapat dengan mudah menemukan contoh penggunaan di backend untuk X86. Jadi ada kelas menengahnya sendiri, dengan bantuan definisi fungsi yang diperlukan sudah dibuat.
Sebagai hasilnya, Anda dapat menggunakan metode
isThreeOperandsLEA
dalam kode C ++.
if (!(TII->isThreeOperandsLEA(MI) || hasInefficientLEABaseReg(Base, Index)) || !TII->isSafeToClobberEFLAGS(MBB, MI) || Segment.getReg() != X86::NoRegister) return;
Di sini TII adalah info instruksi target, yang dapat diperoleh dengan menggunakan metode
getInstrInfo()
dari
MCSubtargetInfo
untuk arsitektur yang diinginkan.
Transformasi instruksi selama optimisasi. Pemetaan instruksi
Selama sejumlah besar optimasi dilakukan pada tahap akhir kompilasi, tugas tersebut sering muncul untuk mengubah semua atau hanya sebagian dari instruksi dari satu formulir menjadi instruksi dari formulir lain. Mengingat penerapan multiclasses yang dijelaskan di awal, kita dapat memiliki sejumlah besar instruksi dengan semantik dan properti yang serupa. Dalam kode, transformasi ini, tentu saja, dapat ditulis dalam bentuk konstruksi
switch-case
besar, yang untuk setiap instruksi menghancurkan transformasi yang sesuai. Sebagian, konstruksi besar ini dapat dikurangi dengan bantuan makro, yang akan membentuk nama instruksi yang diperlukan sesuai dengan aturan yang terkenal. Namun tetap saja, pendekatan ini sangat merepotkan, sulit dipertahankan karena fakta bahwa semua nama instruksi didaftar secara eksplisit. Menambahkan instruksi baru dapat dengan mudah menyebabkan kesalahan, karena Anda harus ingat untuk menambahkannya ke semua konversi yang relevan. Setelah disiksa dengan pendekatan ini, LLVM menciptakan mekanisme khusus untuk secara efisien mengubah satu bentuk instruksi ke
Instruction Mapping
lainnya.
Idenya sangat sederhana, perlu untuk menggambarkan model yang mungkin untuk mengubah instruksi secara langsung di TableGen. Oleh karena itu, dalam TableGen LLVM ada kelas dasar untuk menggambarkan model seperti itu.
class InstrMapping {
Mari kita lihat contoh yang diberikan dalam dokumentasi. Contoh-contoh yang dapat ditemukan dalam kode sumber sekarang bahkan lebih sederhana, karena hanya dua kolom yang diperoleh pada tabel akhir. Dalam kode backend Anda dapat menemukan konversi formulir lama ke bentuk baru instruksi, instruksi dsp dalam mmdsp, dll., Dijelaskan menggunakan Pemetaan Instruksi. Sebenarnya, mekanisme ini tidak begitu banyak digunakan sejauh ini, hanya karena sebagian besar backend mulai dibuat sebelum muncul, dan agar bisa berfungsi, Anda masih perlu mengatur properti yang benar untuk instruksi, jadi beralih ke itu tidak selalu mudah, Anda mungkin memerlukan beberapa refactoring.
Jadi misalnya. Misalkan kita memiliki bentuk instruksi tanpa predikat dan instruksi di mana predikatnya masing-masing benar dan salah. Kami menggambarkannya dengan bantuan multiklass dan kelas khusus, yang hanya akan kami gunakan sebagai filter. Deskripsi yang disederhanakan tanpa parameter dan banyak properti mungkin seperti ini.
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β>; β¦
Dalam contoh ini, omong-omong, juga ditunjukkan bagaimana menimpa properti untuk beberapa definisi sekaligus menggunakan
let β¦ in
konstruk. Sebagai hasilnya, kami memiliki banyak instruksi yang menyimpan nama dasar dan properti mereka yang secara unik menggambarkan formulir mereka. Kemudian Anda dapat membuat model transformasi.
def getPredOpcode : InstrMapping {
Hasilnya, tabel berikut akan dihasilkan dari deskripsi ini.
Suatu fungsi akan dihasilkan dalam file
.inc
int getPredOpcode(uint16_t Opcode, enum PredSense inPredSense)
Dengan demikian, yang menerima kode instruksi untuk konversi dan nilai enumerasi otomatis yang dihasilkan PredSense, yang berisi semua nilai yang mungkin dalam kolom. Implementasi fungsi ini sangat sederhana, karena itu mengembalikan elemen array yang diinginkan untuk instruksi yang menarik bagi kami.
Dan dalam kode backend, alih-alih menulis
switch-case
cukup dengan memanggil fungsi yang dihasilkan, yang akan mengembalikan kode dari instruksi yang dikonversi. Solusi sederhana, di mana menambahkan instruksi baru, tidak akan mengarah pada perlunya tindakan tambahan.
Artefak yang dibuat secara otomatis (file .inc
)
Semua interaksi antara deskripsi TableGen dan kode backend LLVM dipastikan oleh file
.inc
dihasilkan yang berisi kode C. Untuk mendapatkan gambar yang lengkap, mari kita lihat sedikit apa sebenarnya mereka.
Setelah masing-masing membangun, untuk setiap arsitektur, akan ada beberapa file
.inc
dalam direktori build, masing-masing menyimpan potongan informasi terpisah tentang arsitektur.
Jadi ada file <TargetName>GenInstrInfo.inc
yang berisi informasi tentang petunjuk <TargetName>GenRegisterInfo.inc
, masing-masing, yang berisi informasi tentang register, ada file untuk bekerja secara langsung dengan assembler dan output <TargetName>GenAsmMatcher.inc
dan <TargetName>GenAsmWriter.inc
lain-lainJadi terdiri dari apakah file-file ini? Secara umum, mereka berisi enumerasi, array, struktur, dan fungsi sederhana. Misalnya, Anda dapat melihat informasi yang dikonversi pada instruksi di <TargetName>GenInstrInfo.inc
.Pada bagian pertama, di namespace dengan nama target adalah enumerasi yang berisi semua instruksi yang telah dijelaskan. namespace X86 { enum { PHI = 0, β¦ ADD16i16 = 287, ADD16mi = 288, ADD16mi8 = 289, ADD16mr = 290, ADD16ri = 291, ADD16ri8 = 292, ADD16rm = 293, ADD16rr = 294, ADD16rr_REV = 295, β¦ }
Berikutnya adalah array yang menggambarkan properti instruksi const MCInstrDesc X86Insts[]
. Array berikut berisi informasi tentang nama instruksi, dll. Pada dasarnya, semua informasi disimpan dalam transfer dan array.Ada juga fungsi yang telah dijelaskan menggunakan predikat. Berdasarkan definisi predikat fungsi yang dibahas di bagian sebelumnya, fungsi berikut akan dihasilkan. 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; }
Tetapi ada data dalam file dan struktur yang dihasilkan. Di X86GenSubtargetInfo.inc
Anda dapat menemukan contoh struktur yang harus digunakan dalam kode backend untuk mendapatkan informasi tentang arsitektur, melalui itu di bagian sebelumnya ternyata TTI. 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); } };
Jika digunakan Subtarget
untuk menggambarkan berbagai konfigurasi XXXGenSubtarget.inc
, enumerasi akan dibuat dengan properti yang dijelaskan menggunakan SubtargetFeature
array dengan nilai konstan untuk menunjukkan karakteristik dan subtipe CPU, dan sebuah fungsi akan dihasilkan ParseSubtargetFeatures
yang memproses string dengan set opsi Subtarget
. Selain itu, implementasi metode XXXSubtarget
dalam kode backend harus sesuai dengan pseudo-code berikut, di mana perlu untuk menggunakan fungsi ini: XXXSubtarget::XXXSubtarget(const Module &M, const std::string &FS) {
Terlepas dari kenyataan bahwa .inc
file sangat tebal dan mengandung array besar, ini memungkinkan kami untuk mengoptimalkan waktu akses ke informasi, karena mengakses elemen array memiliki waktu yang konstan. Fungsi pencarian yang dihasilkan dengan instruksi diimplementasikan menggunakan algoritma pencarian biner untuk meminimalkan waktu operasi. Jadi penyimpanan dalam bentuk ini cukup dibenarkan.Kesimpulan
Sebagai hasilnya, terima kasih kepada TableGen di LLVM, kami memiliki deskripsi arsitektur yang mudah dibaca dan didukung dalam format tunggal dengan berbagai mekanisme untuk berinteraksi dan mengakses informasi dari kode sumber backend LLVM untuk optimasi dan pembuatan kode. Pada saat yang sama, deskripsi seperti itu tidak mempengaruhi kinerja kompiler karena kode yang dihasilkan secara otomatis yang menggunakan solusi dan struktur data yang efisien.