LLVM هڪ وڃڻ جي نقطه نظر کان

هڪ ڪمپيلر کي ترقي ڪرڻ هڪ تمام ڏکيو ڪم آهي. پر، خوشقسمتيءَ سان، LLVM جهڙن منصوبن جي ترقيءَ سان، هن مسئلي جو حل تمام گهڻو آسان ٿي ويو آهي، جيڪو هڪ پروگرامر کي به هڪ نئين ٻولي ٺاهڻ جي اجازت ڏئي ٿو جيڪا ڪارڪردگيءَ ۾ C جي ويجهو هجي. سسٽم ڪوڊ جي وڏي مقدار جي نمائندگي ڪئي وئي آهي، ٿوري دستاويزن سان ليس. انهيءَ خامي کي درست ڪرڻ جي ڪوشش ڪرڻ لاءِ، مواد جو مصنف، جنهن جو ترجمو اڄ اسان شايع ڪري رهيا آهيون، گو ۾ لکيل ڪوڊ جا مثال ڏيکارڻ وارا آهن ۽ ڏيکاريو ته انهن جو پهريون ترجمو ڪيئن ٿيو. وڃو 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 ڪوڊ گو SSA ڪوڊ کان ٿورڙو مضبوط آھي، C سان ملندڙ جلندڙ آھي. ھتي، فنڪشن ڊڪليئريشن ۾، پھريائين ڊيٽا جي قسم جو بيان آھي اھو موٽائي ٿو، دليل جو قسم دليل جي نالي کان اڳ ڏيکاريو ويو آھي. ان کان علاوه، IR پارسنگ کي آسان ڪرڻ لاء، عالمي ادارن جا نالا علامت کان اڳ آهن @، ۽ مقامي نالن کان اڳ اتي هڪ علامت آهي % (هڪ فنڪشن پڻ عالمي ادارو سمجهيو ويندو آهي).

هن ڪوڊ جي باري ۾ نوٽ ڪرڻ لاء هڪ شيء آهي ته Go جي قسم جي نمائندگي جو فيصلو int، جنهن کي 32-bit يا 64-bit قدر جي طور تي پيش ڪري سگهجي ٿو، مرتب ڪندڙ ۽ تاليف جي ٽارگيٽ تي منحصر ڪري، قبول ڪيو ويندو آهي جڏهن LLVM IR ڪوڊ ٺاهي ٿو. اهو ڪيترن ئي سببن مان هڪ آهي ته LLVM IR ڪوڊ نه آهي، جيئن ڪيترن ئي ماڻهن جو خيال آهي، پليٽ فارم آزاد. اهڙو ڪوڊ، جيڪو هڪ پليٽ فارم لاءِ ٺاهيو ويو آهي، آسانيءَ سان نه ٿو کڻي سگهجي ۽ ٻئي پليٽ فارم لاءِ مرتب ڪيو وڃي (جيستائين توهان هن مسئلي کي حل ڪرڻ لاءِ موزون نه آهيو. انتهائي احتياط سان).

هڪ ٻيو دلچسپ نقطو نوٽ ڪرڻ جي قابل آهي ته قسم i64 هڪ دستخط ٿيل عدد نه آهي: اهو انگ جي نشاني جي نمائندگي ڪرڻ جي لحاظ کان غير جانبدار آهي. هدايتن تي مدار رکندي، اهو ٻنهي نشانين ۽ غير دستخط ٿيل نمبرن جي نمائندگي ڪري سگهي ٿو. اضافي آپريشن جي نمائندگي جي صورت ۾، اهو مسئلو ناهي، تنهنڪري دستخط ٿيل يا غير دستخط ٿيل نمبرن سان ڪم ڪرڻ ۾ ڪو فرق ناهي. هتي مان اهو نوٽ ڪرڻ چاهيان ٿو ته سي ٻولي ۾، هڪ دستخط ٿيل انٽيجر متغير کي اوور فلو ڪرڻ سان اڻڄاتل رويي جي ڪري ٿي، تنهنڪري ڪلانگ فرنٽ اينڊ آپريشن ۾ پرچم شامل ڪري ٿو. nsw (نه دستخط ٿيل لفافي)، جيڪو LLVM کي ٻڌائي ٿو ته اهو فرض ڪري سگهي ٿو ته اضافو ڪڏهن به اوور فلو ناهي.

اهو ٿي سگهي ٿو ڪجهه اصلاحن لاءِ اهم. مثال طور، ٻه قدر شامل ڪرڻ i16 32-bit پليٽ فارم تي (32-bit رجسٽرن سان) جي ضرورت آهي، اضافي کان پوءِ، حد ۾ رهڻ لاءِ سائن توسيع آپريشن 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 فارم ۾ ڪوڊ جي نمائندگي ڪرڻ لاءِ. شايد هن ڪوڊ جي سڀ کان وڌيڪ واضع خصوصيت اها حقيقت آهي ته ڪو به منظم وهڪرو ڪنٽرول حڪم نه آهي. حسابن جي وهڪري کي ڪنٽرول ڪرڻ لاءِ، فقط مشروط ۽ غير مشروط جمپون آهن، ۽، جيڪڏهن اسان هن حڪم کي وهڪري کي ڪنٽرول ڪرڻ لاءِ ڪمانڊ سمجهون ٿا، ته واپسي جو حڪم.

حقيقت ۾، هتي توهان هن حقيقت تي ڌيان ڏئي سگهو ٿا ته پروگرام کي ورهايل بلاڪ ۾ ورهايل نه آهي گھڙي braces (جيئن ته ٻولين جي C خاندان ۾). اهو ليبلن سان ورهايل آهي، اسيمبليء جي ٻولين جي ياد ڏياريندڙ، ۽ بنيادي بلاڪ جي صورت ۾ پيش ڪيو ويو آهي. SSA ۾، بنيادي بلاڪن جي وضاحت ڪئي وئي آھي ڪوڊ جي متضاد ترتيبن جي طور تي جيڪو ھڪڙي ليبل سان شروع ٿئي ٿو ۽ بنيادي بلاڪ مڪمل ڪرڻ جي هدايتن سان ختم ٿئي ٿو، جھڙوڪ - return и jump.

هن ڪوڊ جي هڪ ٻي دلچسپ تفصيل هدايت جي نمائندگي ڪئي وئي آهي phi. هدايتون ڪافي غير معمولي آھن ۽ سمجھڻ ۾ ڪجھ وقت وٺي سگھي ٿو. ياد رکو، اهو ايس ايس جامد سنگل اسائنمينٽ لاءِ مختصر آهي. هي ڪوڊ جي وچولي نمائندگي آهي جيڪو ڪمپلرز پاران استعمال ڪيو ويو آهي، جنهن ۾ هر متغير کي صرف هڪ ڀيرو هڪ قدر مقرر ڪيو ويو آهي. اهو اسان جي فنڪشن وانگر سادي افعال کي ظاهر ڪرڻ لاء وڏو آهي 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 ڪم ڪري ٿو. انساني نقطه نظر کان، هي سڀ ڪوڊ کي سمجهڻ ڏکيو بڻائي ٿو، پر حقيقت اها آهي ته هر قيمت صرف هڪ ڀيرو لڳايو ويو آهي ڪيترن ئي اصلاحن کي تمام آسان بڻائي ٿو.

نوٽ ڪريو ته جيڪڏھن توھان پنھنجو مرتب ڪندڙ لکندا، توھان کي عام طور تي ھن قسم جي شين سان معاملو ڪرڻو پوندو. جيتوڻيڪ ڪلنگ انهن سڀني هدايتن کي پيدا نٿو ڪري 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) هڪ ڍانچي (structure) جي طور تي، پر انهن کي ٽن الڳ ادارن جي طور تي نمائندگي ڪرڻ جي اجازت ڏئي ٿي ڪجهه اصلاحن جي. ٻيا مرتب ڪندڙ شايد ٻين طريقن سان سلائس جي نمائندگي ڪن ٿا، ٽارگيٽ پليٽ فارم جي افعال جي ڪالنگ ڪنوينشن تي منحصر ڪري ٿو.

هن ڪوڊ جي هڪ ٻي دلچسپ خصوصيت جي هدايتن جو استعمال آهي getelementptr (اڪثر ڪري مختصر طور تي GEP).

ھيءَ ھدايت پوائنٽرز سان ڪم ڪري ٿي ۽ استعمال ڪيو ويندو آھي پوائنٽر حاصل ڪرڻ لاءِ ھڪ سلائس عنصر ڏانھن. مثال طور، اچو ته ان کي سي ۾ لکيل هيٺين ڪوڊ سان ڀيٽيون:

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, optimizer ڏانهن اشارو ڪري ٿو ته اهو فنڪشن جيڪو نشان لڳندو آهي اهو موجوده ڪمپليشن يونٽ جي ٻاهران حوالو نه ڏنو ويندو. هي اجازت ڏئي ٿو ڪيترن ئي دلچسپ بين الاقوامي اصلاحن جي لاءِ جيئن غير استعمال ٿيل دليلن کي ختم ڪرڻ.

توھان LLVM بابت وڌيڪ پڙھي سگھو ٿا دستاويز، جنهن کي توهان اڪثر حوالو ڏيندا آهيو جڏهن توهان پنهنجو پنهنجو LLVM-based compiler ٺاهي رهيا آهيو. هتي رهنمائي ڪندڙ، جيڪو هڪ تمام سادي ٻولي لاءِ گڏ ڪرڻ وارو ڪمپلر ٺاهي رهيو آهي. معلومات جا اهي ٻئي ذريعا توهان لاءِ ڪارآمد هوندا جڏهن توهان جو پنهنجو ڪمپلر ٺاهيو.

پيارا پڙهندڙن! ڇا توهان LLVM استعمال ڪري رهيا آهيو؟

LLVM هڪ وڃڻ جي نقطه نظر کان

جو ذريعو: www.habr.com

تبصرو شامل ڪريو