LLVM dari perspektif Go

Membangunkan pengkompil adalah tugas yang sangat sukar. Tetapi, mujurlah, dengan pembangunan projek seperti LLVM, penyelesaian kepada masalah ini sangat dipermudahkan, yang membolehkan walaupun seorang pengaturcara untuk mencipta bahasa baharu yang hampir dalam prestasi C. Bekerja dengan LLVM adalah rumit oleh fakta bahawa ini sistem diwakili oleh sejumlah besar kod, dilengkapi dengan sedikit dokumentasi . Untuk cuba membetulkan kelemahan ini, pengarang bahan, terjemahan yang kami terbitkan hari ini, akan menunjukkan contoh kod yang ditulis dalam Go dan menunjukkan cara ia pertama kali diterjemahkan ke dalam Pergi SSA, dan kemudian dalam LLVM IR menggunakan pengkompil tinyGO. Kod IR Go SSA dan LLVM telah disunting sedikit untuk mengalih keluar perkara yang tidak berkaitan dengan penjelasan yang diberikan di sini, untuk menjadikan penjelasan lebih mudah difahami.

LLVM dari perspektif Go

Contoh pertama

Fungsi pertama yang saya akan lihat di sini ialah mekanisme mudah untuk menambah nombor:

func myAdd(a, b int) int{
    return a + b
}

Fungsi ini sangat mudah, dan, mungkin, tiada yang lebih mudah. Ia diterjemahkan ke dalam kod Go SSA berikut:

func myAdd(a int, b int) int:
entry:
    t0 = a + b                                                    int
    return t0

Dengan paparan ini, pembayang jenis data diletakkan di sebelah kanan dan boleh diabaikan dalam kebanyakan kes.

Contoh kecil ini sudah membolehkan anda melihat intipati satu aspek SSA. Iaitu, apabila menukar kod ke dalam bentuk SSA, setiap ungkapan dipecahkan kepada bahagian paling asas yang mana ia terdiri. Dalam kes kami, arahan return a + b, sebenarnya, mewakili dua operasi: menambah dua nombor dan mengembalikan hasilnya.

Di samping itu, di sini anda boleh melihat blok asas program; dalam kod ini hanya terdapat satu blok - blok kemasukan. Kami akan bercakap lebih lanjut mengenai blok di bawah.

Kod Go SSA dengan mudah ditukar kepada LLVM IR:

define i64 @myAdd(i64 %a, i64 %b) {
entry:
  %0 = add i64 %a, %b
  ret i64 %0
}

Apa yang anda boleh perhatikan ialah walaupun struktur sintaksis yang berbeza digunakan di sini, struktur fungsi itu pada asasnya tidak berubah. Kod IR LLVM adalah lebih kuat sedikit daripada kod Go SSA, serupa dengan C. Di sini, dalam pengisytiharan fungsi, mula-mula terdapat perihalan jenis data yang dikembalikan, jenis argumen ditunjukkan sebelum nama argumen. Di samping itu, untuk memudahkan penghuraian IR, nama entiti global didahului oleh simbol @, dan sebelum nama tempatan terdapat simbol % (fungsi juga dianggap sebagai entiti global).

Satu perkara yang perlu diambil perhatian tentang kod ini ialah keputusan perwakilan jenis Go int, yang boleh diwakili sebagai nilai 32-bit atau 64-bit, bergantung pada pengkompil dan sasaran kompilasi, diterima apabila kod IR LLVM dijana. Ini adalah salah satu daripada banyak sebab bahawa kod IR LLVM tidak, seperti yang difikirkan oleh ramai orang, platform bebas. Kod sedemikian, dicipta untuk satu platform, tidak boleh diambil dan disusun untuk platform lain (melainkan anda sesuai untuk menyelesaikan masalah ini dengan penjagaan yang melampau).

Satu lagi perkara menarik yang perlu diberi perhatian ialah jenis i64 bukan integer bertanda: ia neutral dari segi mewakili tanda nombor. Bergantung pada arahan, ia boleh mewakili kedua-dua nombor yang ditandatangani dan tidak ditandatangani. Dalam kes perwakilan operasi penambahan, ini tidak penting, jadi tidak ada perbezaan dalam bekerja dengan nombor yang ditandatangani atau tidak ditandatangani. Di sini saya ingin ambil perhatian bahawa dalam bahasa C, melimpahi pembolehubah integer yang ditandatangani membawa kepada tingkah laku yang tidak ditentukan, jadi bahagian hadapan Clang menambah bendera pada operasi nsw (tiada bungkus yang ditandatangani), yang memberitahu LLVM bahawa ia boleh menganggap penambahan itu tidak pernah melimpah.

Ini mungkin penting untuk sesetengah pengoptimuman. Sebagai contoh, menambah dua nilai i16 pada platform 32-bit (dengan daftar 32-bit) memerlukan, selepas tambahan, operasi pengembangan tanda untuk kekal dalam julat i16. Oleh sebab itu, selalunya lebih cekap untuk melaksanakan operasi integer berdasarkan saiz daftar mesin.

Apa yang berlaku seterusnya dengan kod IR ini tidak menarik minat kami sekarang. Kod dioptimumkan (tetapi dalam kes contoh mudah seperti kami, tiada apa yang dioptimumkan) dan kemudian ditukar kepada kod mesin.

Contoh kedua

Contoh seterusnya yang akan kita lihat akan menjadi sedikit lebih rumit. Iaitu, kita bercakap tentang fungsi yang menjumlahkan sepotong integer:

func sum(numbers []int) int {
    n := 0
    for i := 0; i < len(numbers); i++ {
        n += numbers[i]
    }
    return n
}

Kod ini ditukar kepada kod Go SSA berikut:

func sum(numbers []int) int:
entry:
    jump for.loop
for.loop:
    t0 = phi [entry: 0:int, for.body: t6] #n                       int
    t1 = phi [entry: 0:int, for.body: t7] #i                       int
    t2 = len(numbers)                                              int
    t3 = t1 < t2                                                  bool
    if t3 goto for.body else for.done
for.body:
    t4 = &numbers[t1]                                             *int
    t5 = *t4                                                       int
    t6 = t0 + t5                                                   int
    t7 = t1 + 1:int                                                int
    jump for.loop
for.done:
    return t0

Di sini anda sudah boleh melihat lebih banyak pembinaan biasa untuk mewakili kod dalam borang SSA. Mungkin ciri yang paling jelas bagi kod ini ialah hakikat bahawa tiada arahan kawalan aliran berstruktur. Untuk mengawal aliran pengiraan, terdapat hanya lompatan bersyarat dan tanpa syarat, dan, jika kita menganggap arahan ini sebagai arahan untuk mengawal aliran, arahan kembali.

Malah, di sini anda boleh memberi perhatian kepada fakta bahawa program ini tidak dibahagikan kepada blok menggunakan pendakap kerinting (seperti dalam keluarga bahasa C). Ia dibahagikan mengikut label, mengingatkan bahasa himpunan, dan dibentangkan dalam bentuk blok asas. Dalam SSA, blok asas ditakrifkan sebagai jujukan kod bersebelahan bermula dengan label dan berakhir dengan arahan penyiapan blok asas, seperti βˆ’ return ΠΈ jump.

Satu lagi butiran menarik kod ini diwakili oleh arahan phi. Arahannya agak luar biasa dan mungkin mengambil sedikit masa untuk difahami. ingat itu SSA ialah singkatan kepada Tugasan Tunggal Statik. Ini ialah perwakilan perantaraan kod yang digunakan oleh penyusun, di mana setiap pembolehubah diberikan nilai sekali sahaja. Ini bagus untuk menyatakan fungsi mudah seperti fungsi kami myAddditunjukkan di atas, tetapi tidak sesuai untuk fungsi yang lebih kompleks seperti fungsi yang dibincangkan dalam bahagian ini sum. Khususnya, pembolehubah berubah semasa pelaksanaan gelung i ΠΈ n.

SSA memintas sekatan untuk memberikan nilai pembolehubah sekali menggunakan arahan yang dipanggil phi (namanya diambil daripada abjad Yunani). Hakikatnya ialah agar perwakilan kod SSA dijana untuk bahasa seperti C, anda perlu menggunakan beberapa helah. Hasil daripada memanggil arahan ini ialah nilai semasa pembolehubah (i atau n), dan senarai blok asas digunakan sebagai parameternya. Sebagai contoh, pertimbangkan arahan ini:

t0 = phi [entry: 0:int, for.body: t6] #n

Maksudnya adalah seperti berikut: jika blok asas sebelumnya adalah blok entry (input), kemudian t0 adalah pemalar 0, dan jika blok asas sebelumnya ialah for.body, maka anda perlu mengambil nilainya t6 dari blok ini. Ini semua mungkin kelihatan agak misteri, tetapi mekanisme inilah yang menjadikan SSA berfungsi. Dari perspektif manusia, ini semua menjadikan kod sukar difahami, tetapi hakikat bahawa setiap nilai diberikan sekali sahaja menjadikan banyak pengoptimuman lebih mudah.

Ambil perhatian bahawa jika anda menulis pengkompil anda sendiri, anda biasanya tidak perlu berurusan dengan perkara seperti ini. Malah Clang tidak menghasilkan semua arahan ini phi, ia menggunakan mekanisme alloca (ia menyerupai bekerja dengan pembolehubah tempatan biasa). Kemudian, apabila menjalankan pas pengoptimuman LLVM dipanggil mem2reg, arahan alloca ditukar kepada borang SSA. TinyGo, bagaimanapun, menerima input daripada Go SSA, yang, dengan mudahnya, sudah ditukar kepada borang SSA.

Satu lagi inovasi serpihan kod perantaraan yang sedang dipertimbangkan ialah akses kepada elemen keratan mengikut indeks diwakili dalam bentuk operasi pengiraan alamat dan operasi penyahrujukan penunjuk yang terhasil. Di sini anda boleh melihat penambahan terus pemalar kepada kod IR (contohnya - 1:int). Dalam contoh dengan fungsi myAdd ini belum digunakan. Memandangkan kita telah mengeluarkan ciri tersebut, mari kita lihat apakah kod ini apabila ditukar kepada bentuk IR LLVM:

define i64 @sum(i64* %ptr, i64 %len, i64 %cap) {
entry:
  br label %for.loop

for.loop:                                         ; preds = %for.body, %entry
  %0 = phi i64 [ 0, %entry ], [ %5, %deref.next ]
  %1 = phi i64 [ 0, %entry ], [ %6, %deref.next ]
  %2 = icmp slt i64 %1, %len
  br i1 %2, label %for.body, label %for.done

for.body:                                         ; preds = %for.loop
  %3 = getelementptr i64, i64* %ptr, i64 %1
  %4 = load i64, i64* %3
  %5 = add i64 %0, %4
  %6 = add i64 %1, 1
  br label %for.loop

for.done:                                         ; preds = %for.loop
  ret i64 %0
}

Di sini, seperti sebelumnya, kita dapat melihat struktur yang sama, yang termasuk struktur sintaksis yang lain. Contohnya, dalam panggilan phi nilai dan label ditukar. Walau bagaimanapun, terdapat sesuatu di sini yang patut diberi perhatian khusus.

Sebagai permulaan, di sini anda boleh melihat tandatangan fungsi yang sama sekali berbeza. LLVM tidak menyokong kepingan, dan akibatnya, sebagai pengoptimuman, pengkompil TinyGo yang menghasilkan kod perantaraan ini membahagikan perihalan struktur data ini kepada beberapa bahagian. Ia boleh mewakili tiga unsur keping (ptr, len ΠΈ cap) sebagai struktur (struct), tetapi mewakilinya sebagai tiga entiti berasingan membolehkan beberapa pengoptimuman. Penyusun lain mungkin mewakili kepingan dalam cara lain, bergantung pada konvensyen panggilan fungsi platform sasaran.

Satu lagi ciri menarik kod ini ialah penggunaan arahan getelementptr (sering disingkat sebagai GEP).

Arahan ini berfungsi dengan penunjuk dan digunakan untuk mendapatkan penunjuk kepada elemen hirisan. Sebagai contoh, mari kita bandingkan dengan kod berikut yang ditulis dalam C:

int* sliceptr(int *ptr, int index) {
    return &ptr[index];
}

Atau dengan persamaan berikut dengan ini:

int* sliceptr(int *ptr, int index) {
    return ptr + index;
}

Perkara yang paling penting di sini ialah arahan getelementptr tidak melakukan operasi penyahrujukan. Ia hanya mengira penunjuk baharu berdasarkan yang sedia ada. Ia boleh diambil sebagai arahan mul ΠΈ add pada peringkat perkakasan. Anda boleh membaca lebih lanjut mengenai arahan GEP di sini.

Satu lagi ciri menarik kod perantaraan ini ialah penggunaan arahan icmp. Ini ialah arahan tujuan umum yang digunakan untuk melaksanakan perbandingan integer. Hasil arahan ini sentiasa nilai jenis i1 - nilai logik. Dalam kes ini, perbandingan dibuat menggunakan kata kunci slt (ditandatangani kurang daripada), kerana kami membandingkan dua nombor yang sebelumnya diwakili oleh jenis int. Jika kita membandingkan dua integer tidak bertanda, maka kita akan menggunakan icmp, dan kata kunci yang digunakan dalam perbandingan ialah ult. Untuk membandingkan nombor titik terapung, arahan lain digunakan, fcmp, yang berfungsi dengan cara yang sama.

Keputusan

Saya percaya bahawa dalam bahan ini saya telah membincangkan ciri-ciri terpenting LLVM IR. Sudah tentu, terdapat banyak lagi di sini. Khususnya, perwakilan perantaraan kod mungkin mengandungi banyak anotasi yang membenarkan pas pengoptimuman untuk mengambil kira ciri tertentu kod yang diketahui oleh pengkompil yang tidak boleh dinyatakan dalam IR. Sebagai contoh, ini adalah bendera inbounds Arahan GEP, atau bendera nsw ΠΈ nuw, yang boleh ditambah pada arahan add. Begitu juga dengan kata kunci private, menunjukkan kepada pengoptimum bahawa fungsi yang ditandakannya tidak akan dirujuk dari luar unit kompilasi semasa. Ini membolehkan banyak pengoptimuman antara prosedur yang menarik seperti menghapuskan hujah yang tidak digunakan.

Anda boleh membaca lebih lanjut mengenai LLVM dalam dokumentasi, yang sering anda rujuk semasa membangunkan pengkompil berasaskan LLVM anda sendiri. Di sini panduan, yang melihat pada membangunkan pengkompil untuk bahasa yang sangat mudah. Kedua-dua sumber maklumat ini akan berguna kepada anda apabila membuat pengkompil anda sendiri.

Pembaca yang dihormati! Adakah anda menggunakan LLVM?

LLVM dari perspektif Go

Sumber: www.habr.com

Tambah komen