جانے کے نقطہ نظر سے LLVM

کمپائلر تیار کرنا بہت مشکل کام ہے۔ لیکن، خوش قسمتی سے، LLVM جیسے منصوبوں کی ترقی کے ساتھ، اس مسئلے کا حل بہت آسان بنا دیا گیا ہے، جو کہ ایک پروگرامر کو بھی ایک نئی زبان بنانے کی اجازت دیتا ہے جو سی کے قریب کارکردگی کے لحاظ سے ہو۔ LLVM کے ساتھ کام کرنا اس حقیقت سے پیچیدہ ہے کہ یہ نظام کو کوڈ کی ایک بہت بڑی رقم کے ذریعہ پیش کیا جاتا ہے، جو بہت کم دستاویزات سے لیس ہے۔ اس خامی کو دور کرنے کی کوشش کرنے کے لیے، مواد کا مصنف، جس کا ترجمہ ہم آج شائع کر رہے ہیں، گو میں لکھے گئے کوڈ کی مثالیں دکھانے جا رہے ہیں اور یہ بتانے جا رہے ہیں کہ ان کا پہلی بار ترجمہ کیسے کیا گیا ہے۔ ایس ایس اے جاؤ، اور پھر کمپائلر کا استعمال کرتے ہوئے 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 فارم میں کوڈ کی نمائندگی کرنے کے لیے مخصوص مزید تعمیرات دیکھ سکتے ہیں۔ شاید اس کوڈ کی سب سے واضح خصوصیت یہ ہے کہ یہاں کوئی ساختہ بہاؤ کنٹرول کمانڈز نہیں ہیں۔ حسابات کے بہاؤ کو کنٹرول کرنے کے لیے، صرف مشروط اور غیر مشروط چھلانگیں ہیں، اور، اگر ہم اس کمانڈ کو بہاؤ کو کنٹرول کرنے کے لیے ایک کمانڈ کے طور پر سمجھتے ہیں، تو واپسی کا حکم۔

درحقیقت، یہاں آپ اس حقیقت پر توجہ دے سکتے ہیں کہ پروگرام کو گھوبگھرالی منحنی خطوط وحدانی (جیسا کہ زبانوں کے 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 اس بلاک سے یہ سب کافی پراسرار معلوم ہوسکتا ہے، لیکن یہ طریقہ کار ایس ایس اے کو کام کرنے کا باعث بنتا ہے۔ انسانی نقطہ نظر سے، یہ سب کوڈ کو سمجھنا مشکل بناتا ہے، لیکن حقیقت یہ ہے کہ ہر قدر کو صرف ایک بار تفویض کیا جاتا ہے، بہت ساری اصلاح کو بہت آسان بنا دیتا ہے۔

نوٹ کریں کہ اگر آپ اپنا کمپائلر لکھتے ہیں، تو آپ کو عام طور پر اس قسم کی چیزوں سے نمٹنا نہیں پڑے گا۔ یہاں تک کہ بجنا بھی ان تمام ہدایات کو تیار نہیں کرتا ہے۔ 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

نیا تبصرہ شامل کریں