Go දෘෂ්ටිකෝණයකින් LLVM

සම්පාදකයක් සංවර්ධනය කිරීම ඉතා අපහසු කාර්යයකි. එහෙත්, වාසනාවකට මෙන්, LLVM වැනි ව්‍යාපෘති සංවර්ධනය කිරීමත් සමඟ, මෙම ගැටලුවට විසඳුම බෙහෙවින් සරල කර ඇත, එමඟින් තනි ක්‍රමලේඛකයෙකුට පවා C කාර්ය සාධනයට ආසන්න නව භාෂාවක් නිර්මාණය කිරීමට ඉඩ සලසයි. LLVM සමඟ වැඩ කිරීම සංකීර්ණ වේ. පද්ධතිය කුඩා ලියකියවිලි වලින් සමන්විත විශාල කේතයකින් නියෝජනය වේ. මෙම අඩුපාඩුව නිවැරදි කිරීමට උත්සාහ කිරීම සඳහා, අපි අද ප්‍රකාශයට පත් කරන ද්‍රව්‍යයේ කතුවරයා, එහි පරිවර්තනය, Go හි ලියා ඇති කේතයේ උදාහරණ නිරූපණය කිරීමට සහ ඒවා මුලින්ම පරිවර්තනය කරන ආකාරය පෙන්වීමට යන්නේ ය. SSA යන්න, පසුව සම්පාදකය භාවිතා කරමින් LLVM IR හි ටයිනිගෝ. Go SSA සහ LLVM IR කේතය මෙහි දක්වා ඇති පැහැදිලි කිරීම්වලට අදාළ නොවන දේවල් ඉවත් කිරීමට, පැහැදිලි කිරීම් වඩාත් අවබෝධ කර ගැනීම සඳහා මඳක් සංස්කරණය කර ඇත.

Go දෘෂ්ටිකෝණයකින් 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 වලට සමානයි. මෙහි, ශ්‍රිත ප්‍රකාශනයේ, පළමුව එය ලබා දෙන දත්ත වර්ගය පිළිබඳ විස්තරයක් ඇත, තර්කයේ නමට පෙර තර්ක වර්ගය දක්වනු ලැබේ. ඊට අමතරව, IR විග්‍රහ කිරීම සරල කිරීම සඳහා, ගෝලීය ආයතනවල නම් සංකේතය මඟින් පෙරාතුව ඇත. @, සහ දේශීය නම් වලට පෙර සංකේතයක් ඇත % (ශ්‍රිතයක් ගෝලීය ආයතනයක් ලෙස ද සැලකේ).

මෙම කේතය ගැන සටහන් කළ යුතු එක් දෙයක් නම් Go ගේ වර්ගය නියෝජනය කිරීමේ තීරණයයි int, 32-bit හෝ 64-bit අගයක් ලෙස නිරූපණය කළ හැකි, සම්පාදකය සහ සම්පාදනයේ ඉලක්කය මත පදනම්ව, LLVM IR කේතය ජනනය කරන විට පිළිගනු ලැබේ. බොහෝ අය සිතන පරිදි LLVM IR කේතය වේදිකා ස්වාධීන නොවීමට මෙය බොහෝ හේතු වලින් එකකි. එක් වේදිකාවක් සඳහා නිර්මාණය කරන ලද එවැනි කේතයක් වෙනත් වේදිකාවක් සඳහා සරලව ගෙන සම්පාදනය කළ නොහැක (ඔබ මෙම ගැටළුව විසඳීමට සුදුසු නොවේ නම්). දැඩි සැලකිල්ලෙන්).

සඳහන් කළ යුතු තවත් සිත්ගන්නා කරුණක් වන්නේ වර්ගයයි i64 එය අත්සන් කරන ලද පූර්ණ සංඛ්‍යාවක් නොවේ: එය සංඛ්‍යාවේ ලකුණ නියෝජනය කිරීමේදී මධ්‍යස්ථ වේ. උපදෙස් මත පදනම්ව, එය අත්සන් කළ සහ අත්සන් නොකළ අංක දෙකම නියෝජනය කළ හැකිය. එකතු කිරීමේ මෙහෙයුම නියෝජනය කිරීමේදී, මෙය වැදගත් නොවේ, එබැවින් අත්සන් කළ හෝ අත්සන් නොකළ අංක සමඟ වැඩ කිරීමේ වෙනසක් නොමැත. මෙහිදී මම සටහන් කිරීමට කැමති C භාෂාවේ, අත්සන් කරන ලද පූර්ණ සංඛ්‍යා විචල්‍යයක් පිටාර ගැලීම නිර්වචනය නොකළ හැසිරීමකට මග පාදයි, එබැවින් ක්ලැන්ග් ඉදිරිපස මෙහෙයුමට ධජයක් එක් කරයි. nsw (අත්සන් නොකළ එතුම), එය LLVM ට පවසන්නේ එකතු කිරීම කිසි විටෙකත් පිටාර ගලන්නේ නැති බව උපකල්පනය කළ හැකි බවයි.

සමහර ප්‍රශස්තකරණයන් සඳහා මෙය වැදගත් විය හැක. උදාහරණයක් ලෙස, අගයන් දෙකක් එකතු කිරීම i16 32-bit වේදිකාවක් මත (බිට් 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 පෝරමයේ කේතය නියෝජනය කිරීම සඳහා සාමාන්‍ය තවත් ඉදිකිරීම් දැකිය හැක. සමහර විට මෙම කේතයේ වඩාත්ම පැහැදිලි ලක්ෂණය වන්නේ ව්‍යුහගත ප්‍රවාහ පාලන විධාන නොමැති වීමයි. ගණනය කිරීම් ගලායාම පාලනය කිරීම සඳහා, කොන්දේසි සහිත සහ කොන්දේසි විරහිත ජම්ප් පමණක් ඇති අතර, මෙම විධානය ප්රවාහය පාලනය කිරීමට විධානයක් ලෙස සලකන්නේ නම්, ආපසු එන විධානයකි.

ඇත්ත වශයෙන්ම, මෙහිදී ඔබට අවධානය යොමු කළ හැකිය වැඩසටහන කැරලි වරහන් භාවිතයෙන් (C භාෂා පවුලෙහි මෙන්) බ්ලොක් වලට බෙදී නොමැත. එය ලේබල් මගින් බෙදනු ලැබේ, එකලස් කිරීමේ භාෂාවන් සිහිගන්වයි, සහ මූලික බ්ලොක් ආකාරයෙන් ඉදිරිපත් කෙරේ. SSA හි, මූලික බ්ලොක් යනු ලේබලයකින් ආරම්භ වන සහ - වැනි මූලික වාරණ සම්පූර්ණ කිරීමේ උපදෙස් වලින් අවසන් වන කේතවල අඛණ්ඩ අනුපිළිවෙල ලෙස අර්ථ දැක්වේ. return и jump.

මෙම කේතයේ තවත් රසවත් විස්තරයක් උපදෙස් මගින් නිරූපණය කෙරේ phi. උපදෙස් තරමක් අසාමාන්‍ය වන අතර තේරුම් ගැනීමට යම් කාලයක් ගතවනු ඇත. මතක තියාගන්න, ඒක එස්එස්ඒ ස්ථිතික තනි පැවරුම සඳහා කෙටි වේ. මෙය සම්පාදකයින් විසින් භාවිතා කරන කේතයේ අතරමැදි නිරූපණයකි, එහි සෑම විචල්‍යයකටම එක් වරක් පමණක් අගයක් පවරනු ලැබේ. අපගේ කාර්යය වැනි සරල කාර්යයන් ප්‍රකාශ කිරීම සඳහා මෙය විශිෂ්ටයි 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 ක්‍රියා කරයි. මානව දෘෂ්ටිකෝණයකින්, මේ සියල්ල කේතය තේරුම් ගැනීමට අපහසු කරයි, නමුත් එක් එක් අගය එක් වරක් පමණක් පැවරීම බොහෝ ප්‍රශස්තකරණයන් වඩාත් පහසු කරයි.

ඔබ ඔබේම සම්පාදකයක් ලියන්නේ නම්, ඔබට සාමාන්‍යයෙන් මෙවැනි දේවල් සමඟ කටයුතු කිරීමට සිදු නොවන බව සලකන්න. ක්ලැන්ග් පවා මෙම සියලු උපදෙස් ජනනය නොකරයි 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 dereferencing මෙහෙයුම් සිදු නොකරයි. එය දැනට පවතින එක මත පදනම්ව නව දර්ශකයක් ගණනය කරයි. එය උපදෙස් ලෙස ගත හැකිය mul и add දෘඪාංග මට්ටමින්. ඔබට GEP උපදෙස් ගැන වැඩිදුර කියවිය හැක මෙහි.

මෙම අතරමැදි කේතයේ තවත් රසවත් ලක්ෂණයක් වන්නේ උපදෙස් භාවිතා කිරීමයි icmp. මෙය පූර්ණ සංඛ්‍යා සැසඳීම් ක්‍රියාවට නැංවීම සඳහා භාවිතා කරන පොදු කාර්ය උපදෙසකි. මෙම උපදෙස් වල ප්රතිඵලය සෑම විටම වර්ගයේ අගයකි i1 - තාර්කික අගය. මෙම අවස්ථාවේදී, ප්රධාන පදය භාවිතයෙන් සංසන්දනයක් සිදු කරනු ලැබේ slt (අඩු අත්සන් කර ඇත), අපි කලින් වර්ගය මගින් නියෝජනය කරන ලද සංඛ්යා දෙකක් සංසන්දනය කරන බැවින් int. අපි අත්සන් නොකළ පූර්ණ සංඛ්‍යා දෙකක් සංසන්දනය කරන්නේ නම්, අපි භාවිතා කරමු icmp, සහ සංසන්දනය කිරීමේදී භාවිතා කරන මූල පදය වනු ඇත ult. පාවෙන ලක්ෂ්‍ය සංඛ්‍යා සංසන්දනය කිරීම සඳහා, තවත් උපදෙස් භාවිතා වේ, fcmp, සමාන ආකාරයකින් ක්රියා කරන.

ප්රතිඵල

මෙම ද්රව්යය තුළ මම LLVM IR හි වඩාත්ම වැදගත් ලක්ෂණ ආවරණය කර ඇති බව මම විශ්වාස කරමි. ඇත්ත වශයෙන්ම, මෙහි තවත් බොහෝ දේ ඇත. විශේෂයෙන්ම, සංග්‍රහයේ අතරමැදි නිරූපණයෙහි IR හි ප්‍රකාශ කළ නොහැකි සම්පාදකයා දන්නා කේතයේ ඇතැම් ලක්ෂණ සැලකිල්ලට ගැනීමට ප්‍රශස්තිකරණ අවසරපත්‍රවලට ඉඩ සලසන බොහෝ විවරණ අඩංගු විය හැක. උදාහරණයක් ලෙස, මෙය කොඩියක් inbounds GEP උපදෙස්, හෝ කොඩි nsw и nuw, උපදෙස් වලට එකතු කළ හැක add. Keyword එකත් එහෙමයි private, එය සලකුණු කරන ශ්‍රිතය වත්මන් සම්පාදන ඒකකයෙන් පිටතින් යොමු නොකරන බව ප්‍රශස්තකරණයට දක්වයි. මෙය භාවිතයට නොගත් තර්ක ඉවත් කිරීම වැනි රසවත් අන්තර් ක්‍රියා පටිපාටි ප්‍රශස්තකරණයන් රාශියකට ඉඩ සලසයි.

ඔබට LLVM ගැන වැඩිදුර කියවිය හැක ලියකියවිලි, ඔබේම LLVM-පාදක සම්පාදකයක් සංවර්ධනය කිරීමේදී ඔබ බොහෝ විට සඳහන් කරනු ඇත. මෙතන නායකත්වය, එය ඉතා සරල භාෂාවක් සඳහා සම්පාදකයක් සංවර්ධනය කිරීම දෙස බලයි. ඔබේම සම්පාදකයක් නිර්මාණය කිරීමේදී මෙම තොරතුරු මූලාශ්‍ර දෙකම ඔබට ප්‍රයෝජනවත් වනු ඇත.

හිතවත් පා readers කයින්! ඔබ LLVM භාවිතා කරන්නේද?

Go දෘෂ්ටිකෝණයකින් LLVM

මූලාශ්රය: www.habr.com

අදහස් එක් කරන්න