LLVM нь Go-ийн хэтийн төлөвөөс

Хөрвүүлэгчийг хөгжүүлэх нь маш хэцүү ажил юм. Гэвч аз болоход LLVM гэх мэт төслүүдийг хөгжүүлснээр энэ асуудлыг шийдэх нь маш хялбаршсан бөгөөд энэ нь нэг програмист ч гэсэн C хэлтэй ойролцоо гүйцэтгэлтэй шинэ хэл үүсгэх боломжийг олгодог. систем нь маш их хэмжээний кодоор илэрхийлэгдэж, бага зэрэг баримт бичигтэй байдаг. Энэхүү дутагдлыг засахын тулд бидний өнөөдрийн орчуулгыг нийтэлж буй материалын зохиогч Go-д бичсэн кодын жишээг үзүүлж, тэдгээрийг хэрхэн анх орчуулж байгааг харуулах гэж байна. SSA руу яв, дараа нь хөрвүүлэгчийг ашиглан LLVM IR-д miniGO. Go SSA болон LLVM IR кодыг тайлбарыг илүү ойлгомжтой болгох үүднээс энд өгөгдсөн тайлбартай хамааралгүй зүйлсийг арилгахын тулд бага зэрэг засварласан.

LLVM нь Go-ийн хэтийн төлөвөөс

Эхний жишээ

Миний энд авч үзэх эхний функц бол тоо нэмэх энгийн механизм юм.

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

Энэ функц нь маш энгийн бөгөөд үүнээс илүү хялбар зүйл байхгүй байж магадгүй юм. Энэ нь дараах Go SSA код руу орчуулагдана:

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

Ийм байдлаар өгөгдлийн төрлийн зөвлөмжийг баруун талд байрлуулсан бөгөөд ихэнх тохиолдолд үл тоомсорлож болно.

Энэхүү жижиг жишээ нь SSA-ийн нэг талын мөн чанарыг олж харах боломжийг танд олгоно. Тодруулбал, кодыг SSA хэлбэрт хөрвүүлэхдээ илэрхийлэл бүрийг бүрдүүлсэн хамгийн энгийн хэсгүүдэд хуваадаг. Манай тохиолдолд тушаал return a + b, үнэн хэрэгтээ хоёр тоог нэмэх, үр дүнг буцаах гэсэн хоёр үйлдлийг илэрхийлдэг.

Нэмж дурдахад, эндээс та програмын үндсэн блокуудыг харж болно, энэ кодонд зөвхөн нэг блок байдаг - нэвтрэх блок. Доор бид блокуудын талаар дэлгэрэнгүй ярих болно.

Go SSA код нь LLVM IR руу амархан хөрвүүлдэг:

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

Таны анзаарсан зүйл бол энд янз бүрийн синтаксийн бүтцийг ашигладаг боловч функцийн бүтэц үндсэндээ өөрчлөгдөөгүй байна. LLVM IR код нь C-тэй төстэй Go SSA кодоос арай илүү хүчтэй юм. Энд функцийн мэдэгдэлд эхлээд буцаах өгөгдлийн төрлийн тайлбар байгаа бөгөөд аргументын нэрийн өмнө аргументын төрлийг зааж өгсөн болно. Нэмж дурдахад, IR задлан шинжлэхийг хялбарчлахын тулд дэлхийн байгууллагуудын нэрийг тэмдэгтийн өмнө тавьдаг @, мөн нутгийн нэрсийн өмнө тэмдэг байдаг % (функцийг мөн дэлхийн нэгж гэж үздэг).

Энэ кодын талаар анхаарах нэг зүйл бол Go-ийн төрлийн төлөөллийн шийдвэр юм intLLVM нь IR код үүсгэх үед хөрвүүлэгч болон эмхэтгэлийн зорилтоос хамааран 32 бит эсвэл 64 битийн утгаар илэрхийлэгдэх боломжтой . Энэ нь олон хүний ​​бодож байгаа шиг LLVM IR код нь платформоос хамааралгүй байх олон шалтгаануудын нэг юм. Нэг платформд зориулж бүтээсэн ийм кодыг өөр платформд зориулж эмхэтгэх боломжгүй (хэрэв та энэ асуудлыг шийдвэрлэхэд тохиромжгүй бол). маш болгоомжтойгоор).

Анхаарах өөр нэг сонирхолтой зүйл бол төрөл юм i64 тэмдэгт бүхэл тоо биш: энэ нь тооны тэмдгийг илэрхийлэх хувьд төвийг сахисан байна. Заавраас хамааран энэ нь гарын үсэг зурсан болон гарын үсэггүй дугаарыг төлөөлж болно. Нэмэх үйлдлийг дүрслэх тохиолдолд энэ нь хамаагүй тул гарын үсэгтэй эсвэл тэмдэггүй тоонуудтай ажиллахад ялгаа байхгүй. Си хэлэнд тэмдэглэгдсэн бүхэл тоон хувьсагчийг хэтрүүлэх нь тодорхойгүй үйлдэлд хүргэдэг тул Clang frontend нь үйл ажиллагаанд туг нэмдэг гэдгийг энд тэмдэглэхийг хүсч байна. nsw (гарын үсэг зурсан боодолгүй) бөгөөд энэ нь LLVM-д нэмэлт хэзээ ч халихгүй гэж үзэж болно гэж хэлдэг.

Энэ нь зарим оновчлолд чухал байж болох юм. Жишээлбэл, хоёр утгыг нэмэх i16 32 битийн платформ дээр (32 битийн регистртэй) хүрээг хадгалахын тулд нэмсний дараа тэмдгийг өргөтгөх үйлдлийг шаарддаг. i16. Ийм учраас машины регистрийн хэмжээн дээр үндэслэн бүхэл тоон үйлдлийг гүйцэтгэх нь ихэвчлэн илүү үр дүнтэй байдаг.

Энэ IR кодын дараа юу болох нь бидний хувьд тийм ч сонирхолтой биш юм. Кодыг оновчтой болгосон (гэхдээ манайх шиг энгийн жишээн дээр юу ч оновчгүй) дараа нь машины код болгон хувиргадаг.

Хоёр дахь жишээ

Бидний авч үзэх дараагийн жишээ нь арай илүү төвөгтэй байх болно. Тухайлбал, бид бүхэл тоонуудын зүсмэлийг нэгтгэдэг функцийн тухай ярьж байна:

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

Энэ код нь дараах Go SSA код руу хөрвүүлдэг:

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

Эндээс та SSA маягт дахь кодыг төлөөлөх бусад бүтцийг аль хэдийн харж болно. Магадгүй энэ кодын хамгийн тод онцлог нь урсгалын удирдлагын бүтэцтэй команд байхгүй байгаа явдал юм. Тооцооллын урсгалыг хянахын тулд зөвхөн нөхцөлт болон болзолгүй үсрэлтүүд байдаг бөгөөд хэрэв бид энэ командыг урсгалыг хянах тушаал гэж үзвэл буцах команд юм.

Үнэн хэрэгтээ энд та програмыг буржгар хаалт ашиглан блок болгон хуваадаггүйг анхаарч үзэх хэрэгтэй (С гэр бүлийн хэлний адил). Энэ нь ассемблер хэлийг санагдуулам шошгон дээр хуваагдаж, үндсэн блок хэлбэрээр үзүүлэв. SSA-д үндсэн блокууд нь шошготой эхэлж блок дуусгах үндсэн зааварчилгаагаар төгсдөг, жишээлбэл - return и jump.

Энэ кодын өөр нэг сонирхолтой нарийн ширийн зүйлийг зааварчилгаагаар дүрсэлсэн болно phi. Зааврууд нь ер бусын бөгөөд ойлгоход хэсэг хугацаа шаардагдана. гэдгийг санах SSA нь Static Single Assignment гэсэн үгийн товчлол юм. Энэ нь хөрвүүлэгчдийн ашигладаг кодын завсрын дүрслэл бөгөөд хувьсагч бүрт зөвхөн нэг удаа утга оноодог. Энэ нь бидний функц шиг энгийн функцуудыг илэрхийлэхэд тохиромжтой myAddдээр харуулсан боловч энэ хэсэгт авч үзсэн функц зэрэг илүү төвөгтэй функцүүдэд тохиромжгүй sum. Ялангуяа давталтыг гүйцэтгэх явцад хувьсагчид өөрчлөгддөг i и n.

SSA нь зааврыг ашиглан хувьсагчийн утгыг нэг удаа оноох хязгаарлалтыг давдаг. phi (түүний нэрийг Грек цагаан толгойноос авсан). Үнэн хэрэгтээ C гэх мэт хэлэнд зориулсан SSA-ийн кодын дүрслэлийг бий болгохын тулд та зарим заль мэхийг ашиглах хэрэгтэй. Энэ зааврыг дуудсаны үр дүн нь хувьсагчийн одоогийн утга юм (i буюу n), үндсэн блокуудын жагсаалтыг түүний параметр болгон ашигладаг. Жишээлбэл, энэ зааврыг авч үзье:

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

Үүний утга нь дараах байдалтай байна: хэрэв өмнөх үндсэн блок нь блок байсан бол entry (оролт), дараа нь t0 тогтмол юм 0, мөн өмнөх үндсэн блок байсан бол for.body, дараа нь та утгыг авах хэрэгтэй t6 энэ блокоос. Энэ бүхэн маш нууцлаг мэт санагдаж болох ч энэ механизм нь SSA-г ажиллуулдаг. Хүний өнцгөөс харахад энэ бүхэн кодыг ойлгоход хэцүү болгодог ч утга тус бүрийг зөвхөн нэг удаа оноож байгаа нь олон оновчлолыг илүү хялбар болгодог.

Хэрэв та өөрийн хөрвүүлэгчийг бичвэл ихэвчлэн ийм төрлийн зүйлтэй ажиллах шаардлагагүй болно гэдгийг анхаарна уу. Clang хүртэл эдгээр бүх зааврыг үүсгэдэггүй phi, энэ нь механизмыг ашигладаг alloca (энэ нь ердийн орон нутгийн хувьсагчтай ажиллахтай төстэй). Дараа нь LLVM оновчлолын дамжуулалтыг ажиллуулж байх үед дуудагдсан mem2reg, зааварчилгаа alloca SSA маягт руу хөрвүүлэв. Гэсэн хэдий ч TinyGo нь Go SSA-аас оролтыг хүлээн авдаг бөгөөд энэ нь аль хэдийн SSA маягт руу хөрвүүлэгдсэн байдаг.

Хэлэлцэж буй завсрын кодын фрагментийн өөр нэг шинэлэг зүйл бол зүсмэлийн элементүүдэд индексээр хандах хандалтыг хаягийг тооцоолох үйлдэл болон үүссэн заагчаас ялгах үйлдлээр илэрхийлдэг. Эндээс та IR код руу тогтмол нэмэхийг харж болно (жишээ нь - 1:int). Функц бүхий жишээнд myAdd үүнийг ашиглаагүй байна. Одоо бид эдгээр функцуудыг замаас нь гаргасан тул LLVM IR хэлбэрт хөрвүүлэхэд энэ код юу болохыг харцгаая.

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
}

Эндээс өмнөх шигээ бусад синтаксийн бүтцийг багтаасан ижил бүтцийг харж болно. Жишээлбэл, дуудлагад phi үнэ цэнэ, шошгыг сольсон. Гэсэн хэдий ч энд онцгой анхаарал хандуулах ёстой нэг зүйл бий.

Эхлэхийн тулд та огт өөр функцийн гарын үсгийг харж болно. LLVM нь зүсмэлүүдийг дэмждэггүй бөгөөд үр дүнд нь оновчлолын хувьд энэхүү завсрын кодыг үүсгэсэн TinyGo хөрвүүлэгч энэ өгөгдлийн бүтцийн тайлбарыг хэсэг болгон хуваасан. Энэ нь гурван зүсмэл элементийг төлөөлж болно (ptr, len и cap) бүтэц (бүтэц) байдлаар, гэхдээ тэдгээрийг гурван тусдаа байгууллага болгон төлөөлөх нь зарим оновчлолыг хийх боломжийг олгодог. Бусад хөрвүүлэгчид зорилтот платформын функцүүдийн дуудлагын конвенциос хамааран зүсмэлийг өөр аргаар төлөөлж болно.

Энэ кодын бас нэг сонирхолтой онцлог бол зааврыг ашиглах явдал юм getelementptr (ихэвчлэн GEP гэж товчилдог).

Энэ заавар нь заагчтай ажилладаг бөгөөд зүсмэл элемент рүү заагч авахад хэрэглэгддэг. Жишээлбэл, үүнийг C хэл дээр бичсэн дараах кодтой харьцуулж үзье.

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

Эсвэл үүнтэй ижил төстэй:

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

Энд хамгийн чухал зүйл бол заавар юм getelementptr лавлагааг хасах үйлдлийг гүйцэтгэдэггүй. Энэ нь одоо байгаа нэг заагч дээр үндэслэн шинэ заагчийг тооцоолдог. Үүнийг заавар болгон авч болно mul и add техник хангамжийн түвшинд. Та GEP зааврын талаар илүү ихийг уншиж болно энд.

Энэхүү завсрын кодын бас нэг сонирхолтой онцлог бол зааврыг ашиглах явдал юм icmp. Энэ нь бүхэл тоон харьцуулалтыг хэрэгжүүлэхэд ашигладаг ерөнхий зориулалтын заавар юм. Энэ зааврын үр дүн нь үргэлж төрлийн утга юм i1 - логик утга. Энэ тохиолдолд түлхүүр үг ашиглан харьцуулалт хийнэ slt (-аас бага гарын үсэг зурсан), учир нь бид өмнө нь төрлөөр илэрхийлэгдсэн хоёр тоог харьцуулж байна int. Хэрэв бид хоёр тэмдэггүй бүхэл тоог харьцуулж үзвэл бид ашиглах болно icmp, мөн харьцуулалтад ашигласан түлхүүр үг байх болно ult. Хөвөгч цэгийн тоог харьцуулахын тулд өөр зааврыг ашиглана. fcmp, энэ нь ижил төстэй байдлаар ажилладаг.

Үр дүн

Энэ материалд би LLVM IR-ийн хамгийн чухал шинж чанаруудыг тусгасан гэдэгт би итгэдэг. Мэдээжийн хэрэг, энд илүү олон зүйл бий. Ялангуяа кодын завсрын дүрслэл нь хөрвүүлэгчийн мэддэг кодын тодорхой шинж чанаруудыг харгалзан оновчтой болгох боломжийг олгодог олон тэмдэглэгээг агуулж болох бөгөөд үүнийг IR-д өөрөөр илэрхийлэх боломжгүй юм. Жишээлбэл, энэ бол туг далбаа юм inbounds GEP заавар, эсвэл туг nsw и nuw, үүнийг зааварт нэмж оруулах боломжтой add. Түлхүүр үгэнд мөн адил хамаарна private, оновчлогчдод тэмдэглэсэн функцийг одоогийн эмхэтгэлийн нэгжийн гаднаас лавлахгүй гэдгийг харуулж байна. Энэ нь ашиглагдаагүй аргументуудыг арилгах гэх мэт олон сонирхолтой процедур хоорондын оновчтой болгох боломжийг олгодог.

Та LLVM-ийн талаар илүү ихийг уншиж болно баримт бичигLLVM дээр суурилсан хөрвүүлэгчийг боловсруулахдаа та үүнийг байнга дурдах болно. Энд гарын авлага, энэ нь маш энгийн хэлэнд зориулсан хөрвүүлэгчийг хөгжүүлэхийг хардаг. Эдгээр мэдээллийн эх сурвалжууд хоёулаа өөрийн хөрвүүлэгчийг үүсгэхэд хэрэг болно.

Эрхэм уншигчид! Та LLVM ашиглаж байна уу?

LLVM нь Go-ийн хэтийн төлөвөөс

Эх сурвалж: www.habr.com

сэтгэгдэл нэмэх