கோ கண்ணோட்டத்தில் LLVM

ஒரு கம்பைலரை உருவாக்குவது மிகவும் கடினமான பணி. ஆனால், அதிர்ஷ்டவசமாக, LLVM போன்ற திட்டங்களின் வளர்ச்சியுடன், இந்த சிக்கலுக்கான தீர்வு மிகவும் எளிமைப்படுத்தப்பட்டுள்ளது, இது ஒரு புரோகிராமர் கூட C க்கு நெருக்கமான ஒரு புதிய மொழியை உருவாக்க அனுமதிக்கிறது. LLVM உடன் பணிபுரிவது சிக்கலானது. கணினி ஒரு பெரிய அளவிலான குறியீட்டால் குறிப்பிடப்படுகிறது, சிறிய ஆவணங்களுடன் பொருத்தப்பட்டுள்ளது. இந்த குறைபாட்டை சரிசெய்வதற்காக, பொருளின் ஆசிரியர், இன்று நாம் வெளியிடும் மொழிபெயர்ப்பானது, Go இல் எழுதப்பட்ட குறியீட்டின் எடுத்துக்காட்டுகளை விளக்கி, அவை முதலில் எவ்வாறு மொழிபெயர்க்கப்படுகின்றன என்பதைக் காட்டப் போகிறது செல்க SSA, பின்னர் கம்பைலரைப் பயன்படுத்தி LLVM IR இல் டைனிகோ. கோ எஸ்எஸ்ஏ மற்றும் எல்எல்விஎம் ஐஆர் குறியீடு, இங்கு கொடுக்கப்பட்டுள்ள விளக்கங்களுக்குப் பொருந்தாத விஷயங்களை நீக்கி, விளக்கங்களை மேலும் புரியவைக்கும் வகையில் சிறிது திருத்தப்பட்டுள்ளது.

கோ கண்ணோட்டத்தில் LLVM

முதல் உதாரணம்

நான் இங்கே பார்க்கப் போகும் முதல் செயல்பாடு எண்களைச் சேர்ப்பதற்கான எளிய வழிமுறை:

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 ஐப் போன்றது. இங்கே, செயல்பாடு அறிவிப்பில், முதலில் அது வழங்கும் தரவு வகையின் விளக்கம் உள்ளது, வாதத்தின் பெயருக்கு முன் வாத வகை குறிப்பிடப்படுகிறது. கூடுதலாக, ஐஆர் பாகுபடுத்தலை எளிதாக்க, உலகளாவிய நிறுவனங்களின் பெயர்கள் சின்னத்தால் முன்வைக்கப்படுகின்றன. @, மற்றும் உள்ளூர் பெயர்களுக்கு முன் ஒரு சின்னம் உள்ளது % (ஒரு செயல்பாடு உலகளாவிய நிறுவனமாகவும் கருதப்படுகிறது).

இந்தக் குறியீட்டைப் பற்றி கவனிக்க வேண்டிய ஒன்று, Go இன் வகை பிரதிநிதித்துவ முடிவு int, 32-பிட் அல்லது 64-பிட் மதிப்பாகக் குறிப்பிடப்படலாம், இது கம்பைலர் மற்றும் தொகுப்பின் இலக்கைப் பொறுத்து, LLVM ஐஆர் குறியீட்டை உருவாக்கும் போது ஏற்றுக்கொள்ளப்படுகிறது. பலர் நினைப்பது போல் LLVM IR குறியீடு இயங்காமல் இருப்பதற்கு இதுவும் ஒன்று. ஒரு தளத்திற்காக உருவாக்கப்பட்ட அத்தகைய குறியீட்டை, மற்றொரு தளத்திற்கு எடுத்து தொகுக்க முடியாது (இந்தச் சிக்கலைத் தீர்ப்பதற்கு நீங்கள் பொருத்தமானவராக இல்லாவிட்டால். மிகுந்த கவனத்துடன்).

கவனிக்க வேண்டிய மற்றொரு சுவாரஸ்யமான விஷயம் என்னவென்றால், வகை i64 கையொப்பமிடப்பட்ட முழு எண் அல்ல: எண்ணின் அடையாளத்தைக் குறிக்கும் வகையில் இது நடுநிலையானது. அறிவுறுத்தலைப் பொறுத்து, இது கையொப்பமிடப்பட்ட மற்றும் கையொப்பமிடப்படாத எண்களைக் குறிக்கலாம். கூட்டல் செயல்பாட்டின் பிரதிநிதித்துவத்தின் விஷயத்தில், இது ஒரு பொருட்டல்ல, எனவே கையொப்பமிடப்பட்ட அல்லது கையொப்பமிடப்படாத எண்களுடன் வேலை செய்வதில் எந்த வித்தியாசமும் இல்லை. C மொழியில், கையொப்பமிடப்பட்ட முழு எண் மாறியை நிரம்பி வழிவது வரையறுக்கப்படாத நடத்தைக்கு வழிவகுக்கிறது என்பதை இங்கே நான் கவனிக்க விரும்புகிறேன், எனவே க்ளாங் ஃபிரண்டெண்ட் செயல்பாட்டிற்கு ஒரு கொடியை சேர்க்கிறது. nsw (கையொப்பமிடப்பட்ட மடக்கு இல்லை), இது எல்.எல்.வி.எம்-க்கு கூறுகிறது, கூட்டல் ஒருபோதும் நிரம்பி வழிவதில்லை.

சில மேம்படுத்தல்களுக்கு இது முக்கியமானதாக இருக்கலாம். எடுத்துக்காட்டாக, இரண்டு மதிப்புகளைச் சேர்த்தல் i16 32-பிட் பிளாட்ஃபார்மில் (32-பிட் பதிவேடுகளுடன்) வரம்பில் இருக்க, சேர்த்த பிறகு, ஒரு அடையாள விரிவாக்க செயல்பாடு தேவைப்படுகிறது. i16. இதன் காரணமாக, இயந்திரப் பதிவு அளவுகளின் அடிப்படையில் முழு எண் செயல்பாடுகளைச் செய்வது மிகவும் திறமையானது.

இந்த ஐஆர் குறியீட்டில் அடுத்து என்ன நடக்கும் என்பது இப்போது எங்களுக்கு குறிப்பாக ஆர்வமாக இல்லை. குறியீடு உகந்ததாக உள்ளது (ஆனால் எங்களுடையது போன்ற எளிய உதாரணத்தில், எதுவும் உகந்ததாக இல்லை) பின்னர் இயந்திரக் குறியீடாக மாற்றப்படும்.

இரண்டாவது உதாரணம்

நாம் பார்க்கப்போகும் அடுத்த உதாரணம் இன்னும் கொஞ்சம் சிக்கலானதாக இருக்கும். அதாவது, முழு எண்களின் ஒரு பகுதியைத் தொகுக்கும் ஒரு செயல்பாட்டைப் பற்றி நாங்கள் பேசுகிறோம்:

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. அறிவுறுத்தல்கள் மிகவும் அசாதாரணமானவை மற்றும் புரிந்துகொள்ள சிறிது நேரம் ஆகலாம். அதை நினைவில் கொள் எஸ்எஸ்ஏ ஸ்டேடிக் சிங்கிள் அசைன்மென்ட் என்பதன் சுருக்கம். இது கம்பைலர்களால் பயன்படுத்தப்படும் குறியீட்டின் இடைநிலை பிரதிநிதித்துவமாகும், இதில் ஒவ்வொரு மாறிக்கும் ஒரு முறை மட்டுமே மதிப்பு ஒதுக்கப்படும். எங்கள் செயல்பாடு போன்ற எளிய செயல்பாடுகளை வெளிப்படுத்த இது சிறந்தது myAddமேலே காட்டப்பட்டுள்ளது, ஆனால் இந்த பிரிவில் விவாதிக்கப்பட்ட செயல்பாடு போன்ற மிகவும் சிக்கலான செயல்பாடுகளுக்கு ஏற்றது அல்ல sum. குறிப்பாக, லூப்பின் செயல்பாட்டின் போது மாறிகள் மாறுகின்றன i и n.

SSA ஆனது அறிவுறுத்தல் என்று அழைக்கப்படும் ஒரு முறை பயன்படுத்தி மாறி மதிப்புகளை ஒதுக்குவதற்கான கட்டுப்பாட்டை மீறுகிறது. phi (அதன் பெயர் கிரேக்க எழுத்துக்களில் இருந்து எடுக்கப்பட்டது). உண்மை என்னவென்றால், சி போன்ற மொழிகளுக்கான குறியீட்டின் SSA பிரதிநிதித்துவத்தை உருவாக்க, நீங்கள் சில தந்திரங்களை நாட வேண்டும். இந்த அறிவுறுத்தலை அழைப்பதன் விளைவாக மாறியின் தற்போதைய மதிப்பு (i அல்லது n), மற்றும் அடிப்படைத் தொகுதிகளின் பட்டியல் அதன் அளவுருக்களாகப் பயன்படுத்தப்படுகிறது. எடுத்துக்காட்டாக, இந்த அறிவுறுத்தலைக் கவனியுங்கள்:

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

அதன் பொருள் பின்வருமாறு: முந்தைய அடிப்படை தொகுதி ஒரு தொகுதியாக இருந்தால் entry (உள்ளீடு), பின்னர் t0 ஒரு நிலையானது 0, மற்றும் முந்தைய அடிப்படை தொகுதி என்றால் for.body, பின்னர் நீங்கள் மதிப்பை எடுக்க வேண்டும் t6 இந்த தொகுதியில் இருந்து. இவை அனைத்தும் மிகவும் மர்மமானதாக தோன்றலாம், ஆனால் இந்த பொறிமுறையே SSA வேலை செய்ய வைக்கிறது. மனிதக் கண்ணோட்டத்தில், இவை அனைத்தும் குறியீட்டைப் புரிந்துகொள்வதை கடினமாக்குகிறது, ஆனால் ஒவ்வொரு மதிப்பும் ஒரு முறை மட்டுமே ஒதுக்கப்படுவது பல மேம்படுத்தல்களை மிகவும் எளிதாக்குகிறது.

உங்கள் சொந்த கம்பைலரை நீங்கள் எழுதினால், பொதுவாக இதுபோன்ற விஷயங்களை நீங்கள் சமாளிக்க வேண்டியதில்லை என்பதை நினைவில் கொள்க. கணகண வென்ற சப்தம் கூட இந்த வழிமுறைகளை உருவாக்கவில்லை phi, இது ஒரு பொறிமுறையைப் பயன்படுத்துகிறது alloca (இது சாதாரண உள்ளூர் மாறிகளுடன் வேலை செய்வதை ஒத்திருக்கிறது). பின்னர், ஒரு LLVM தேர்வுமுறை பாஸ் எனப்படும் இயங்கும் போது mem2reg, அறிவுறுத்தல்கள் alloca SSA படிவத்திற்கு மாற்றப்பட்டது. இருப்பினும், TinyGo, Go SSA இலிருந்து உள்ளீட்டைப் பெறுகிறது, இது வசதியாக, ஏற்கனவே SSA படிவத்திற்கு மாற்றப்பட்டுள்ளது.

பரிசீலனையில் உள்ள இடைநிலை குறியீட்டின் துண்டின் மற்றொரு கண்டுபிடிப்பு என்னவென்றால், குறியீட்டின் மூலம் ஸ்லைஸ் உறுப்புகளுக்கான அணுகல் முகவரியைக் கணக்கிடும் செயல்பாட்டின் வடிவத்தில் குறிப்பிடப்படுகிறது மற்றும் அதன் விளைவாக வரும் சுட்டிக்காட்டியைக் குறைக்கும் செயல்பாடு. ஐஆர் குறியீட்டில் மாறிலிகளை நேரடியாகச் சேர்ப்பதை இங்கே காணலாம் (உதாரணமாக - 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) ஒரு கட்டமைப்பாக (struct), ஆனால் அவற்றை மூன்று தனித்தனி நிறுவனங்களாகக் குறிப்பிடுவது சில மேம்படுத்தல்களை அனுமதிக்கிறது. இலக்கு இயங்குதளத்தின் செயல்பாடுகளின் அழைப்பு மரபுகளைப் பொறுத்து மற்ற கம்பைலர்கள் ஸ்லைஸை வேறு வழிகளில் குறிப்பிடலாம்.

இந்த குறியீட்டின் மற்றொரு சுவாரஸ்யமான அம்சம் அறிவுறுத்தலின் பயன்பாடு ஆகும் getelementptr (பெரும்பாலும் GEP என சுருக்கப்படுகிறது).

இந்த அறிவுறுத்தல் சுட்டிகளுடன் வேலை செய்கிறது மற்றும் ஒரு ஸ்லைஸ் உறுப்புக்கு ஒரு சுட்டியைப் பெறப் பயன்படுகிறது. எடுத்துக்காட்டாக, C இல் எழுதப்பட்ட பின்வரும் குறியீட்டுடன் ஒப்பிடுவோம்:

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

அல்லது இதற்குச் சமமான பின்வருவனவற்றைக் கொண்டு:

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

இங்கே மிக முக்கியமான விஷயம் அறிவுறுத்தல்கள் getelementptr dereferencing செயல்பாடுகளை செய்யாது. இது ஏற்கனவே உள்ளதை அடிப்படையாகக் கொண்டு ஒரு புதிய சுட்டியைக் கணக்கிடுகிறது. அதை அறிவுறுத்தல்களாக எடுத்துக் கொள்ளலாம் mul и add வன்பொருள் மட்டத்தில். GEP வழிமுறைகளைப் பற்றி மேலும் படிக்கலாம் இங்கே.

இந்த இடைநிலை குறியீட்டின் மற்றொரு சுவாரஸ்யமான அம்சம் அறிவுறுத்தலின் பயன்பாடு ஆகும் icmp. இது முழு எண் ஒப்பீடுகளைச் செயல்படுத்தப் பயன்படுத்தப்படும் பொது நோக்கத்திற்கான அறிவுறுத்தலாகும். இந்த அறிவுறுத்தலின் முடிவு எப்போதும் வகையின் மதிப்பாகும் i1 - தருக்க மதிப்பு. இந்த வழக்கில், முக்கிய சொல்லைப் பயன்படுத்தி ஒரு ஒப்பீடு செய்யப்படுகிறது slt (குறைவாக கையொப்பமிடப்பட்டுள்ளது), ஏனெனில் நாங்கள் முன்னர் குறிப்பிடப்பட்ட வகையின் இரண்டு எண்களை ஒப்பிடுகிறோம் int. கையொப்பமிடாத இரண்டு முழு எண்களை நாம் ஒப்பிட்டுப் பார்த்தால், நாம் பயன்படுத்துவோம் icmp, மற்றும் ஒப்பீட்டில் பயன்படுத்தப்படும் முக்கிய சொல் ult. மிதக்கும் புள்ளி எண்களை ஒப்பிட, மற்றொரு வழிமுறை பயன்படுத்தப்படுகிறது, fcmp, இது அதே வழியில் செயல்படுகிறது.

முடிவுகளை

இந்த உள்ளடக்கத்தில் நான் LLVM IR இன் மிக முக்கியமான அம்சங்களை உள்ளடக்கியிருக்கிறேன் என்று நம்புகிறேன். நிச்சயமாக, இங்கே இன்னும் நிறைய இருக்கிறது. குறிப்பாக, குறியீட்டின் இடைநிலைப் பிரதிநிதித்துவம், IR இல் வெளிப்படுத்த முடியாத, தொகுப்பாளருக்குத் தெரிந்த குறியீட்டின் சில அம்சங்களைக் கணக்கில் எடுத்துக்கொள்ள, தேர்வுமுறை பாஸை அனுமதிக்கும் பல சிறுகுறிப்புகளைக் கொண்டிருக்கலாம். உதாரணமாக, இது ஒரு கொடி inbounds GEP வழிமுறைகள் அல்லது கொடிகள் nsw и nuw, இது வழிமுறைகளில் சேர்க்கப்படலாம் add. முக்கிய வார்த்தைக்கும் இதுவே செல்கிறது private, அது குறிக்கும் செயல்பாடு தற்போதைய தொகுத்தல் அலகுக்கு வெளியே இருந்து குறிப்பிடப்படாது என்பதை மேம்படுத்திக்குக் குறிக்கிறது. இது பயன்படுத்தப்படாத வாதங்களை நீக்குவது போன்ற பல சுவாரஸ்யமான இடைச்செயல்முறை மேம்படுத்தல்களை அனுமதிக்கிறது.

நீங்கள் LLVM பற்றி மேலும் படிக்கலாம் ஆவணங்கள், உங்களின் சொந்த LLVM-அடிப்படையிலான கம்பைலரை உருவாக்கும் போது நீங்கள் அடிக்கடி குறிப்பிடுவீர்கள். இங்கே வழிகாட்டி, இது மிகவும் எளிமையான மொழிக்கான தொகுப்பியை உருவாக்குவதைப் பார்க்கிறது. உங்கள் சொந்த கம்பைலரை உருவாக்கும் போது இந்த இரண்டு தகவல் ஆதாரங்களும் உங்களுக்கு பயனுள்ளதாக இருக்கும்.

அன்புள்ள வாசகர்கள்! நீங்கள் LLVM பயன்படுத்துகிறீர்களா?

கோ கண்ணோட்டத்தில் LLVM

ஆதாரம்: www.habr.com

கருத்தைச் சேர்