LLVM Go көз карашынан

Компиляторду иштеп чыгуу абдан татаал иш. Бирок, бактыга жараша, LLVM сыяктуу долбоорлорду иштеп чыгуу менен, бул маселени чечүү абдан жөнөкөйлөштүрүлгөн, бул бир гана программистке C тилине жакын жаңы тилди түзүүгө мүмкүндүк берет. LLVM менен иштөө бул татаалдашат. системасы аз документтер менен жабдылган коддун зор көлөмү менен берилген. Бул кемчиликти оңдоого аракет кылуу үчүн, биз бүгүн котормосун жарыялап жаткан материалдын автору Go программасында жазылган коддун мисалдарын көрсөтүп, алардын биринчи жолу кантип которулганын көрсөтөт. SSA барыңыз, анан компиляторду колдонуу менен LLVM IRде tinyGO. 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 коду Go SSA кодунан бир аз күчтүүрөөк, C га окшош. Бул жерде функциянын декларациясында алгач ал кайтарып берген маалымат түрүнүн сыпаттамасы бар, аргументтин түрү аргументтин аталышынын алдында көрсөтүлөт. Кошумчалай кетсек, IR талдоону жөнөкөйлөтүү үчүн глобалдык объектилердин аталыштарынын алдында символ коюлат @, ал эми жергиликтүү аталыштардын алдында символ бар % (функция глобалдык объект катары да каралат).

Бул код жөнүндө бир нерсе белгилей кетүү керек, бул Go типтеги өкүлчүлүк чечими intLLVM IR кодун түзгөндө, компиляторго жана компиляциянын максатына жараша 32-бит же 64-биттик маани катары көрсөтүлүшү мүмкүн. Бул LLVM IR коду көп адамдар ойлогондой, платформадан көз карандысыз эмес экендигинин көптөгөн себептеринин бири. Бир платформа үчүн түзүлгөн мындай кодду башка платформа үчүн кабыл алып, түзүүгө болбойт (эгерде сиз бул маселени чечүүгө ылайыктуу болбосоңуз өтө кылдаттык менен).

Белгилей кетчү дагы бир кызыктуу жагдай бул түрү i64 белгиси коюлган бүтүн сан эмес: сандын белгисин көрсөтүү жагынан нейтралдуу. Инструкцияга жараша ал кол коюлган жана кол коюлбаган сандарды да көрсөтө алат. Толуктоо операциясын көрсөтүүдө бул маанилүү эмес, ошондуктан кол коюлган же кол коюлбаган сандар менен иштөөдө эч кандай айырма жок. Бул жерде мен Си тилинде кол коюлган бүтүн өзгөрмөнүн ашып кетиши аныкталбаган жүрүм-турумга алып келерин белгилеп кетким келет, андыктан Clang фронту операцияга желек кошот. 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 (анын аты грек алфавитинен алынган). Чындыгында, коддун SSA өкүлчүлүгү C сыяктуу тилдер үчүн жаралышы үчүн, сиз кээ бир амалдарды колдонушуңуз керек. Бул нускаманы чакыруунун натыйжасы өзгөрмөнүн учурдагы мааниси (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 көз карашынан

Source: www.habr.com

Комментарий кошуу