గో కోణం నుండి LLVM

కంపైలర్‌ను అభివృద్ధి చేయడం చాలా కష్టమైన పని. కానీ, అదృష్టవశాత్తూ, LLVM వంటి ప్రాజెక్ట్‌ల అభివృద్ధితో, ఈ సమస్యకు పరిష్కారం చాలా సులభతరం చేయబడింది, ఇది ఒక ప్రోగ్రామర్‌ని కూడా C పనితీరులో దగ్గరగా ఉండే కొత్త భాషను సృష్టించడానికి అనుమతిస్తుంది. LLVMతో పని చేయడం సంక్లిష్టంగా ఉంటుంది. సిస్టమ్ చిన్న డాక్యుమెంటేషన్‌తో కూడిన భారీ మొత్తంలో కోడ్ ద్వారా ప్రాతినిధ్యం వహిస్తుంది. ఈ లోపాన్ని సరిదిద్దడానికి ప్రయత్నించడానికి, ఈ రోజు మనం ప్రచురిస్తున్న మెటీరియల్ రచయిత, గోలో వ్రాసిన కోడ్ యొక్క ఉదాహరణలను ప్రదర్శించబోతున్నారు మరియు అవి మొదట ఎలా అనువదించబడ్డాయో చూపించబోతున్నారు. వెళ్ళండి SSA, ఆపై కంపైలర్‌ని ఉపయోగించి LLVM IRలో టినిగో. వివరణలను మరింత అర్థమయ్యేలా చేయడానికి, ఇక్కడ ఇచ్చిన వివరణలకు సంబంధం లేని అంశాలను తీసివేయడానికి 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 మాదిరిగానే ఉంటుంది. ఇక్కడ, ఫంక్షన్ డిక్లరేషన్‌లో, మొదట అది తిరిగి ఇచ్చే డేటా రకం యొక్క వివరణ ఉంది, వాదన పేరు ముందు వాదన రకం సూచించబడుతుంది. అదనంగా, IR పార్సింగ్‌ను సరళీకృతం చేయడానికి, గ్లోబల్ ఎంటిటీల పేర్లకు ముందు గుర్తు ఉంటుంది @, మరియు స్థానిక పేర్లకు ముందు ఒక చిహ్నం ఉంటుంది % (ఒక ఫంక్షన్ కూడా గ్లోబల్ ఎంటిటీగా పరిగణించబడుతుంది).

ఈ కోడ్ గురించి గమనించాల్సిన విషయం ఏమిటంటే, గో రకం ప్రాతినిధ్య నిర్ణయం int, కంపైలర్ మరియు సంకలనం యొక్క లక్ష్యాన్ని బట్టి 32-బిట్ లేదా 64-బిట్ విలువగా సూచించబడుతుంది, LLVM IR కోడ్‌ను రూపొందించినప్పుడు అంగీకరించబడుతుంది. LLVM IR కోడ్ చాలా మంది అనుకుంటున్నట్లుగా, ప్లాట్‌ఫారమ్ స్వతంత్రంగా ఉండకపోవడానికి ఇది చాలా కారణాలలో ఒకటి. ఒక ప్లాట్‌ఫారమ్ కోసం సృష్టించబడిన ఇటువంటి కోడ్, మరొక ప్లాట్‌ఫారమ్ కోసం తీసుకోబడదు మరియు కంపైల్ చేయబడదు (ఈ సమస్యను పరిష్కరించడానికి మీరు సరిపోకపోతే తీవ్ర జాగ్రత్తతో).

గమనించదగ్గ మరో ఆసక్తికరమైన అంశం ఏమిటంటే రకం i64 సంతకం చేయబడిన పూర్ణాంకం కాదు: సంఖ్య యొక్క చిహ్నాన్ని సూచించే పరంగా ఇది తటస్థంగా ఉంటుంది. సూచనపై ఆధారపడి, ఇది సంతకం మరియు సంతకం చేయని సంఖ్యలను సూచిస్తుంది. అదనపు ఆపరేషన్ యొక్క ప్రాతినిధ్యం విషయంలో, ఇది పట్టింపు లేదు, కాబట్టి సంతకం లేదా సంతకం చేయని సంఖ్యలతో పని చేయడంలో తేడా లేదు. ఇక్కడ నేను C భాషలో, సంతకం చేసిన పూర్ణాంకం వేరియబుల్‌ని ఓవర్‌ఫ్లో చేయడం నిర్వచించబడని ప్రవర్తనకు దారితీస్తుందని గమనించదలిచాను, కాబట్టి క్లాంగ్ ఫ్రంటెండ్ ఆపరేషన్‌కు ఫ్లాగ్‌ని జోడిస్తుంది 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 స్టాటిక్ సింగిల్ అసైన్‌మెంట్ కోసం చిన్నది. ఇది కంపైలర్లు ఉపయోగించే కోడ్ యొక్క ఇంటర్మీడియట్ ప్రాతినిధ్యం, దీనిలో ప్రతి వేరియబుల్ ఒకసారి మాత్రమే విలువను కేటాయించబడుతుంది. మా ఫంక్షన్ వంటి సాధారణ ఫంక్షన్‌లను వ్యక్తీకరించడానికి ఇది చాలా బాగుంది 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) ఒక నిర్మాణంగా (struct), కానీ వాటిని మూడు వేర్వేరు ఎంటిటీలుగా సూచించడం కొన్ని ఆప్టిమైజేషన్‌లను అనుమతిస్తుంది. ఇతర కంపైలర్‌లు లక్ష్య ప్లాట్‌ఫారమ్ ఫంక్షన్‌ల కాలింగ్ సంప్రదాయాలపై ఆధారపడి, ఇతర మార్గాల్లో స్లైస్‌ను సూచించవచ్చు.

ఈ కోడ్ యొక్క మరొక ఆసక్తికరమైన లక్షణం సూచనల ఉపయోగం 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

మూలం: www.habr.com

ఒక వ్యాఖ్యను జోడించండి