آشنایی با طراحي، ساختار و مزاياي llvm
نمایش نتایج: از شماره 1 تا 6 , از مجموع 6

موضوع: آشنایی با طراحي، ساختار و مزاياي llvm

  1. *Mohammad* آواتار ها
    *Mohammad*
    مدیر سابق
    May 2011
    63,336
    22,637
    تشکر شده : 91,444

    پیش فرض آشنایی با طراحي، ساختار و مزاياي llvm

    تولید و توسعه کامپایلرها یکی از پیچیده‌ترین و سنگین‌ترین فرآیندهای برنامه‌نویسی است. پیاده‌سازی صحیح یک کامپایلر مستلزم داشتن درکی عمیق از پارادایم‌های برنامه‌نویسی، شناخت سخت‌افزار و نحوه کارکرد ماشین‌ها و همین‌طور توانایی و استعدادی زیاد در حوزه ریاضیات و منطق است. به همین دلیل، تعداد کامپایلرهای موفق و مطرح، به ویژه آن‌هایی که با هدف گرفتن پلتفرم‌های مختلف توانسته‌اند به جامعه کاربری گسترده‌ای دست یابند، چندان زیاد نیست. طبیعی است که توسعه و نگه‌داری چنین پروژه‌هایی نیز همواره بر عهده شرکت‌ها و نهادهای بزرگ و قدرتمند نرم‌افزاری بوده است.
    پیچیدگی کامپایلرها از یک سو و وابستگی اجزای مختلف آن از سوی دیگر باعث شده است تا دستکاری، ایجاد تغییر و مشارکت در توسعه کامپایلرها (حتی در نمونه‌های آزادی نظیر GCC) تنها محدود به درصد اندکی از توسعه‌دهندگان باشد. علاوه بر این، یکپارچگی کامپایلر، امکان بازنویسی و استفاده دوباره از یک کامپایلر برای پشتیبانی از زبان‌های برنامه‌نویسی جدید یا پلتفرم‌های سخت‌افزاری تازه را دشوار و گاهی ناممکن می‌سازد. اینجا است که طراحی ماجولار کامپایلری به‌نسبت جوان با نام LLVM مزیت‌هایی را به دنبال خواهد داشت که می‌تواند توسعه و بهینه‌سازی کامپایلرها را ساده‌تر کرده و دایره مشارکت‌کنندگان و استفاده‌کنندگان آن را گسترده‌تر سازد. پروژه LLVM مجموعه‌ای از فناوری‌های کامپایلر و زنجیره ابزارهای توسعه ماجولار است که بر‌خلاف نامش، ارتباط چندانی با ماشین‌های مجازی ندارد. این کامپایلر به زبان C++ و برای بهینه‌سازی (Optimization) کدهای زبان ماشین در زمان کامپایل، زمان لینک کردن و حتی زمان اجرا نوشته شده است. این پروژه در سال 2000 در دانشگاه ایلی‌نویز و به رهبری ویکرام ادوه (Vikram Adve) و کریس لاتنر (Chris Lattner) کلید خورد و هدف اصلی آن ایجاد زیرساختی برای انجام تحقیقات در زمینه کامپایل دینامیک بود. این پروژه از ابتدا زیرپوشش مجوز اپن‌سورس دانشگاه ایلی‌نویز که شبیه مجوزهای BSD است، برای استفاده عموم عرضه شده است. اگرچه LLVM به زبان C++ و در ابتدا برای خانواده زبان‌های C نوشته شده بود، اما ذات مستقل و ماجولار این کامپایلر که به زبان خاصی وابسته نبود، زمینه را برای ساخت Frontendهای متنوع آماده ساخته و امکان استفاده از LLVM برای کامپایل زبان‌های فراوانی از جمله آدا، هسکل، پایتون، روبی، فرترن و... روی پلتفرم‌های سخت‌افزاری مختلف را نیز فراهم کرد. برای درک تأثیر و اهمیت این پروژه کافی است در نظر داشته باشیم که شرکت اپل در سال 2005، لاتنر را به استخدام خود درآورد و گروهی را برای کار روی LLVM و استفاده از آن در توسعه نرم‌افزارهای اپل تشکیل داد. از آن زمان، LLVM یکی از اجزای اصلی ابزارهای توسعه، چه در سکوی Mac OS X و چه در iOS بوده است. آن‌چه در ادامه می‌آید، بخشی از کتاب «معماری برنامه‌های اپن‌سورس» (Architecture of Open Source Applications) است.
    طی پنج سال گذشته، LLVM از پروژه‌ای آکادمیک به Back-end جهانی کامپایلرهای C++ ، C و Objective C تبدیل شده است. کلید این موفقیت، کارایی، انعطاف‌پذیری و سازگاری آن است که هر دوی این خصوصیات از طراحی و پیاده‌سازی منحصربه‌فرد آن نشأت گرفته است.پروژه LLVM چتری است که مجموعه‌ای از بخش‌های مرتبط و سطح پایین زنجیره ابزارهای توسعه (نظیر اسمبلرها، کامپایلرها، دیباگرها و...) را در سایه خود میزبانی کرده و توسعه می‌دهد. این ابزارها به‌گونه‌ای طراحی شده‌اند که با ابزارهای کنونی و موجود مورد استفاده در سکوی یونیکس سازگار باشند. نام LLVM زمانی سرنام «Low Level Virtual Machine» بود، اما اکنون به برندی برای پروژه مادر تبدیل شده است. در حالی‌که LLVM ویژگی‌های منحصربه‌فردی را عرضه می‌کند و به‌واسطه استفاده گسترده از برخی ابزارهایش (نظیر Clang که کامپایلری برای C++، C و Objective C است و نسبت به GCC مزیت‌های خاص خود را دارد) شناخته می‌شود، اما مهم‌ترین وجه تمایز آن با سایر کامپایلرها، معماری درونی آن است. از شروع پروژه در دسامبر 2000 میلادی، LLVM به صورت مجموعه‌ای از کتابخانه‌ها با قابلیت استفاده دوباره و رابط‌هایی (Interface) کاملاً تعریف شده، طراحی شد. در آن زمان پیاده‌سازی‌های اپن‌سورس زبان‌های برنامه‌نویسی، به‌عنوان یک ابزار تک منظوره طراحی می‌شدند. به عنوان مثال، استفاده دوباره از parser یک کامپایلر استاتیک نظیر GCC، برای انجام تحلیل‌های آماری و refactoring بسیار دشوار بود. در حالی‌که زبان‌های اسکریپتی معمولاً راهی را برای تعبیه مفسر و کتابخانه‌های زمان اجرای‌شان در نرم‌افزارهای بزرگتر فراهم می‌کردند، این کتابخانه‌های زمان اجرا معمولاً قطعه‌ای حجیم و یکپارچه از کد بود که می‌توانست در نرم‌افزار قرار داده شده یا از آن حذف شود. راهی برای استفاده دوباره از قسمت‌های مختلف آن‌ها وجود نداشت و اشتراکات اندکی میان پیاده‌سازی‌های مختلف یک زبان دیده می‌شد.
    فراتر از چیدمان اجزای خود کامپایلر، جامعه کاربری شکل گرفته پیرامون پیاده‌سازی‌های محبوب زبان‌ها نیز معمولاً به شدت دو قطبی بود. یک پیاده‌سازی خاص، یا یک کامپایلر استاتیک مانند GCC،Free Pascal یا Free BASIC را عرضه می‌کرد یا به عرضه یک کامپایلر زمان اجرا (در قالب یک مفسر یا کامپایلر JIT) می‌پرداخت. پیاده‌سازی‌هایی که هر دوی این قابلیت‌ها را عرضه کنند، به ندرت دیده می‌شدند و در صورت وجود، اشتراک اندکی میان کدهای نوشته شده برای این دو وضعیت وجود داشت. در ده سال اخیر اما، LLVM این چشم‌انداز را از اساس دگرگون کرده است. اکنون از LLVM به عنوان زیرساختی برای پیاده‌سازی گستره وسیعی از کامپایلرهای استاتیک و زمان‌اجرا استفاده می‌شود. این گستره وسیع، خانواده زبان‌هایی را که توسط GCC، جاوا، دات‌نت، پایتون، روبی، اسکیم، هسکل وD پشتیبانی می‌شوند و همین‌طور بسیاری از زبان‌های کمتر شناخته‌شده دیگر را شامل می‌شود. کامپایلر LLVM همچنین جایگزین بسیاری از کامپایلرهای با استفاده خاص نیز شده است. از میان این کامپایلرها می‌توان به موتور بهینه‌سازی زمان اجرای OpenGL در سیستم‌عامل اپل، کتابخانه‌های پردازش تصویر و پشته‌ها در After Effects ادوبی اشاره کرد. در‌نهایت LLVM برای تولید محصولات جدید و متنوعی نیز مورد استفاده قرار گرفته است که شناخته‌شده‌ترین آن‌ها شاید OpenCL و کتابخانه‌های زمان اجرای آن باشد.

    آشنایی با طراحی کلاسیک کامپایلرها
    معمول‌ترین طراحی برای یک کامپایلر استاتیک سنتی (مانند بیشتر کامپایلرهای C) یک طراحی سه مرحله‌ای است که اجزای اصلی آن Frontend، بخش بهینه‌سازی یا Optimizer و Backend هستند.(شکل 1) بخش Frontend وظیفه parse کردن کد منبع، کنترل آن برای خطاها و ایرادات و ساخت یک «درخت نحوی انتزاعی» («AST » سرنام
    Abstract Syntax Tree که نمایشی انتزاعی از کد ورودی است) را برعهده دارد. در برخی شرایط خود AST برای بهینه‌سازی در بخش Optimizer به فرم جدیدی تبدیل می‌شود و Optimizer و Backend این کد را اجرا می‌کنند.(شکل1)
    بخش‌های اصلی یک کامپایلر سه بخشی
    وظیفه Optimizer این است که در راستای بهبود کارایی کد در زمان اجرا، تغییرات متنوعی را در آن به وجود آورد؛ مثلاً محاسبات اضافی را حذف کند. این قسمت کم‌وبیش از زبان مورد استفاده و معماری ماشین مقصد مستقل است. پس از آن، قسمت Backend (که به مولد کد یا Code Generator نیز مشهور است) کد را به مجموعه دستورالعمل‌های ماشین مقصد نگاشت می‌کند. علاوه بر ایجاد کد صحیح و بدون خطا، ایجاد کدهای خوب و سریع نیز بر عهده این قسمت است. به این معنی که قسمت Backend باید بتواند از قابلیت‌های اختصاصی معماری موردنظر بهره برده و کدهای بهینه‌تری ایجاد کند. قسمت‌های معمول Backend یک کامپایلر، بخش‌های انتخاب دستورالعمل، تخصیص ثبات (Register) و زمان‌بندی دستورالعمل‌ها هستند. این مدل، درست به همین شکل برای مفسرها و کامپایلرهای JIT نیز صادق است. ماشین مجازی جاوا (JVM) نیز یکی از انواع پیاده‌سازی‌های این مدل است که از بایت‌کدهای جاوا به عنوان رابطی (Interface) میان Frontend و Optimizer بهره می‌برد.

    پیاده‌سازی این طراحی
    مهم‌ترین برگ برنده این طراحی کلاسیک، زمانی رو می‌شود که یک کامپایلر، بخواهد چندین زبان برنامه‌نویسی یا چندین معماری مقصد را پشتیبانی کند. همان‌طور که در شکل 2 مشاهده می‌کنید، در چنین صورتی اگر کامپایلر از یک نمایش یا نحوه ارائه یکسان در Optimizer استفاده کند، می‌توان برای هر یک از زبان‌های موردنظر یک Frontend جداگانه نوشت که کدهای آن زبان را به فرم مورد استفاده در Optimizer تبدیل کند و پس از آن برای هر معماری موردنظر نیز یک Backend جداگانه ایجاد کرد تا حاصل کار Optimizer را به کدهای آن معماری ترجمه کند. (شکل 2)

    هدف گرفتن چندین معماری مقصد
    با این سیستم طراحی، پورت‌کردن یک کامپایلر برای پشتیبانی از یک زبان جدید (مثل Algol یا BASIC) تنها به پیاده‌سازی یک Frontend جدید نیاز دارد، اما Optimizer و Backend موجود به همان شکل قابل استفاده هستند. اگر این بخش‌ها از هم جدا نشده بودند، کار با یک زبان برنامه‌نویسی جدید مستلزم ایجاد همه چیز از صفر بود و به این ترتیب برای پشتیبانی از n زبان برنامه‌نویسی و m معماری سخت‌افزاری به m*n کامپایلر نیاز بود.یکی دیگر از مزیت‌های طراحی سه مرحله‌ای (که به‌طور مستقیم نتیجه قابلیت هدف‌گیری چندین معماری مختلف است) این است که کامپایلر می‌تواند در خدمت مجموعه وسیع‌تری از برنامه‌نویسان باشد و نه تنها برنامه‌نویسان یک زبان خاص با معماری مقصد یکسان. برای یک پروژه اپن‌سورس، این به معنای جامعه بزرگتری از مشارکت‌کنندگان احتمالی است که خود باعث بهبود یافتن و پیشرفت سریع‌تر کامپایلر خواهد شد. درست به همین دلیل است که کامپایلرهای اپن‌سورس (نظیر GCC) که توسط جوامع کاربری فراوانی مورد استفاده قرار می‌گیرند، معمولاً نسبت به کامپایلرهای خاص‌تر (نظیر Free Pascal) کدهای ماشین بهینه‌تری تولید می‌کنند. اما این قضیه در مورد کامپایلرهای غیر اپن‌سورس اختصاصی صحت ندارد. کیفیت چنین کامپایلرهایی مستقیماً به بودجه پروژه وابسته خواهد بود. به عنوان مثال کامپایلر ICC اینتل (هرچند طیف مخاطبان اندکی دارد) به واسطه کیفیت کدی که تولید می‌کند، بسیار مشهور شده است.در نهایت، یکی از مهم‌ترین برتری‌های طراحی سه‌مرحله‌ای این است که مهارت‌های لازم برای ایجاد Frontend با مهارت‌های مورد نیاز برای ایجاد Optimizer و Backend کاملاً متفاوت هستند. با جداسازی این بخش‌ها، نگه‌داری و ارتقای بخش Frontend برای برنامه‌نویسان آن قسمت ساده‌تر خواهد بود. اگرچه این موردی اجتماعی به شمار می‌رود، در عمل بسیار حیاتی خواهد بود، به ویژه در پروژه‌های اپن‌سورس که سعی دارند موانع را به حداقل برسانند تا بتوانند بیشترین میزان مشارکت را جذب کنند.

    پیاده‌سازی‌های زبان‌های موجود
    با این‌که مزایا و فواید این طراحی سه مرحله‌ای بسیار وسوسه‌کننده هستند و به‌خوبی در کتاب‌های آموزشی مربوط به کامپایلرها مدون شده‌اند، در عمل این طراحی هیچ‌گاه به صورت کامل پیاده نمی‌شود. با نگاهی به پیاده‌سازی‌های زبان‌های اپن‌سورس (البته در گذشته یعنی زمانی که LLVM کار خود را آغاز کرد) مشاهده خواهید کرد که پیاده‌سازی‌های پرل، پایتون، روبی و جاوا هیچ کد مشترکی ندارند! علاوه بر این، خواهید دید که پروژه‌هایی نظیر GHC (سرنام Glasgow Haskell Compiler) یا FreeBASIC می‌توانند پردازنده‌های مختلفی را هدف بگیرند، اما پیاده‌سازی آن‌ها تنها منحصر به پشتیبانی از یک زبان برنامه‌نویسی خاص است. همچنین فناوری‌های متنوعی برای پیاده‌سازی کامپایلرهای خاص و تک منظوره نظیر کامپایلرهای JIT مخصوص پردازش تصویر، RegEx، درایورهای کارت‌های گرافیک و سایر حوزه‌هایی که به انجام پردازش‌های فراوان توسط پردازنده وابسته هستند، مورد استفاده قرار می‌گیرد. با همه این‌ها، سه شیوه موفق استفاده از این مدل طراحی را می‌توان مشاهده کرد. شیوه نخست در جاوا و ماشین‌های مجازی دات‌نت قابل مشاهده است. این سیستم‌ها یک کامپایلر JIT، پشتیبانی از Runtime و یک قالب بایت‌کد با تعریف بسیار دقیق و مناسب را فراهم می‌کنند. در این صورت هر زبانی که بتواند به آن بایت‌کد کامپایل شود (که تعدادشان هم به شدت زیاد است) می‌تواند از توانایی‌ها و قابلیت‌های Optimizer، JIT و همین‌طور Runtime استفاده کند.
    نکته منفی چنین پیاده‌سازی‌هایی این است که انعطاف‌پذیری اندکی در انتخاب Runtime دارند. هر دو مورد کامپایل JIT، Garbage Collection و استفاده از یک مدل خاص اشیا (Object Model) را به کاربر تحمیل می‌کنند. این امر باعث می‌شود که زبان‌هایی نظیر C (مثلاً در پروژه LLVM) که به خوبی با این مدل سازگار نیستند، به کارایی حداکثری خود دست نیابند. شیوه دوم (که شاید بدشانس‌ترین آن‌ها و در عین حال پرکاربردترین روش برای استفاده دوباره از فناوری کامپایلر باشد) ترجمه کد منبع به زبان C (یا سایر زبان‌ها) و ارسال آن به کامپایلرهای موجود C است.
    این کار امکان استفاده دوباره از Optimizer و Backend یا مولد کد را فراهم آورده کنترل و انعطاف‌پذیری خوبی را برای Runtime ممکن می‌سازد و درک، پیاده‌سازی و نگه‌داری آن را برای برنامه‌نویسان Frontend بسیار ساده می‌کند. متأسفانه چنین کاری امکان پیاده‌سازی کارای سیستم مدیریت استثنا (Exception Handling) را از بین برده، تجربه دیباگ ناخوشایندی را برای کاربر به ارمغان می‌آورد و فرآیند کامپایل را کند می‌کند. علاوه‌بر این، اگر زبانی که مورد استفاده قرار می‌گیرد، نیازمند قابلیت‌هایی باشد که در C وجود ندارند (مثلاً Tail Callهای تضمین شده) این امر می‌تواند دردسر ساز باشد.آخرین شیوه موفق پیاده‌سازی چنین مدلی، GCC (سرنام GNU Compiler Collection) است. کامپایلر GCC از Frontendها و Backendهای متنوعی پشتیبانی کرده و جامعه بزرگ و فعالی از مشارکت‌کنندگان را در کنار خود دارد. این کامپایلر مدت‌ها تنها یک کامپایلر‌‌C بود که می‌توانست معماری‌های متنوعی را هدف بگیرد و با هک‌های مختلف، تعداد اندکی از زبان‌های برنامه‌نویسی دیگر را هم پشتیبانی می‌کرد. با گذشت زمان، جامعه GCC به تدریج به طرحی تمیزتر و بهتر دست یافت.
    در نسخه 4/4، GCC برای Optimizer از نحوه نمایش کدی استفاده می‌کند که GIMPLE Tuples نامیده می‌شود و بسیار بیش از نمونه‌های قبلی از Fontend مستقل شده است. همچنین Frontendهای آدا و فرترن آن از ASTهای تمیز و پالوده‌ای بهره می‌برند. این سه شیوه پیاده‌سازی در عین موفقیت فراوان، محدودیت‌های زیادی در موارد استفاده و کاربرد دارند. دلیل این محدودیت‌ها این است که به عنوان برنامه‌هایی یکپارچه طراحی شده‌اند. به عنوان یک نمونه، در کاربردهای واقعی نمی‌توان GCC را به صورت توکار در برنامه دیگری گنجاند، از آن به عنوان یک کامپایلر JIT یا Runtime استفاده کرد یا قسمت‌هایی از آن را بدون نیاز به بخش‌های دیگر بیرون کشیده و مورد استفاده دوباره قرار داد. افرادی که می‌خواستند از Frontend زبان C++ در GCC برای ایجاد مستندات، نشانه‌گذاری کد، Refactoring و ابزارهای تحلیل آماری استفاده کنند، مجبور بودند از GCC به عنوان یک برنامه یکپارچه استفاده کنند که اطلاعات جالب و مفید را در قالب XML بیرون می‌داد یا باید پلاگینی می‌نوشتند که کدهای خارجی را در پردازه GCC تزریق می‌کرد. دلایل متعددی وجود دارد که باعث می‌شود نتوان از اجزای GCC به‌عنوان کتابخانه‌های مستقل استفاده دوباره به عمل آورد. استفاده فراوان از متغیرهای جهانی، استفاده اختیاری از ثابت‌ها، ساختارهای داده‌ای با طراحی ضعیف، کدهای مبنای پراکنده و نابسامان و در نهایت استفاده از ماکروها، از جمله این دلایل هستند. به عنوان مثال، استفاده از ماکروها باعث می‌شود که کدهای مبنا تنها به گونه‌ای کامپایل شوند که در هر زمان تنها یک Frontend و یک Backend پشتیبانی شود. هرچند مشکلی که اصلاح آن از همه سخت‌تر است، مشکلات ارثی معماری است که از ریشه‌های اولیه و قدمت زیاد این کامپایلر سرچشمه می‌گیرند.
    کامپایلر GCC به صورت خاص از لایه‌بندی و انتزاع‌های ضعیف رنج می‌برد. قسمت Backend کدهای AST تولید شده توسط Frontend را پیمایش می‌کند تا اطلاعات دیباگ را تولید کند، قسمت Frontend ساختارهای داده‌ای مرتبط با Backend را به وجود می‌آورد و کل کامپایلر به ساختارهای داده جهانی (Global) متکی است که توسط رابط خط فرمان تنظیم می‌شوند.

    نردبان این جهان ما و منیست
    عاقبت این
    نردبان افتادنیست
    لاجرم آن کس که بالاتر نشست
    استخوانش سخت تر خواهد شکست




    #1 ارسال شده در تاريخ 11th November 2012 در ساعت 10:28

  2. *Mohammad* آواتار ها
    *Mohammad*
    مدیر سابق
    May 2011
    63,336
    22,637
    تشکر شده : 91,444

    پیش فرض

    حوه نمایش کدهای LLVM یا LLVM IR
    حال که از پس‌زمینه‌های تاریخی و شرایط موجود سخن گفتیم، باید به سراغ LLVM برویم. مهم‌ترین خصوصیت طراحی LLVM چیزی است که نحوه نمایش میانی، Intermediate Representation یا به اختصار IR نامیده می‌شود و در واقع قالبی است که برای نمایش و ارائه کد در کامپایلر مورد استفاده قرار می‌گیرد. این نحوه نمایش به گونه‌ای طراحی شده است که میزبان تغییر شکل‌ها و تحلیل‌های میانی باشد که در بخش Optimizer کامپایلرهای معمول مشاهده می‌شود. در طراحی LLVM IR اهداف خاصی مد نظر قرار گرفته‌اند که پشتیبانی از بهینه‌سازی‌های زمان اجرا (Runtime Optimization)، تحلیل‌های مرتبط با کل برنامه، بهینه‌سازی‌های میان تابعی(Cross-function/Interprocedural Optimization)، تغییرهــــای ساختاری تهاجمی (Aggressive Restructuring Transformation) از آن جمله‌اند. مهم‌ترین جنبه چنین رویکردی این است که خود IR به صورت زبانی در رده اول (First Class Language زبانی است که بتواند با توابع به‌عنوان آرگومان ورودی یا مقدار خروجی توابع دیگر کار کند. در این زبان‌ها میان تابع و متغیر تفاوتی وجود ندارد) با سمانتیکی کاملاً تعریف شده، طراحی شده است. برای واضح‌تر شدن این مطلب به نمونه‌ای که در فهرست 1 آورده شده است توجه کنید.


    define i32 @add1
    (i32 %a, i32 %b)
    {
    entry
    :
    %tmp1 = add i32 %a, %b
    ret i32 %tmp1
    }
    define i32 @add2<i32 %a, i32 %b
    (
    {
    entry
    :
    %tmp1 = icmp eq i32 %a, 0
    br i1 %tmp1, label %done, label %recurse
    recurse
    :
    %tmp2 = sub i32 %a, 1
    %tmp3 = add i32 %b, 1
    %tmp4 = call i32 @add2
    (i32 %tmp2, i32 %tmp3)
    ret i32 %tmp4
    done
    :
    ret i32 %b
    {
    این کد LLVM IR، ترجمه تابعی به زبان C است که آن را در فهرست2 مشاهده می‌کنید. این قطعه کد دو روش متفاوت را برای جمع کردن دو عدد صحیح تعریف می‌کند.



    unsigned add1
    (unsigned a, unsigned b)
    {
    return a+b
    ;
    }
    //
    Perhaps not the most efficient way to add two
    numbers
    unsigned add2
    (unsigned a, unsigned b)
    {
    if (a == 0) return b
    ;
    return add2
    (a-1, b+1)
    ;
    {
    همان‌طور که در این مثال مشاهده می‌کنید، LLVM IR در واقع نوعی مجموعه دستورالعمل مجازی، شبیه مجموعه دستورالعمل‌های RISC است. درست همانند یک مجموعه دستورالعمل RISC واقعی، LLVM IR نیز از توالی ساده دستورالعمل‌هایی نظیر جمع، تفریق، مقایسه و انشعاب پشتیبانی می‌کند. این مجموعه دستورالعمل‌ها در قالب دستورالعمل‌های سه آدرسی یا Three Address قرار دارند به این معنا که می‌توانند ورودی‌های متعددی را از چندین رجيستر گرفته و نتیجه را در ثباتی دیگر ثبت کنند. این درست برخلاف روشی است که در معماری‌های دو آدرسی نظیر x86 استفاده می‌شود.
    در این معماری‌ها، اطلاعات رجيستر ورودی بازنویسی شده و از بین می‌رود. در ماشین‌های تک آدرسی سیستم تنها یک عملوند مشخص را دریافت کرده و عملیات را روی یک آکومولاتور یا یک پشته (Stack) به‌انجام می‌رساند. علاوه بر این LLVM IR از برچسب‌ها (Label) پشتیبانی می‌کند و به صورت کلی مانند فرم عجیب و غریبی از زبان اسمبلی به نظر می‌رسد. برخلاف بیشتر مجموعه دستورالعمل‌های RISC، در دستورالعمل‌های LLVM،‌انتخاب نوع داده مورد استفاده از میان یک سیستم نوع داده ساده اجباری است. به عبارت دیگر LLVM را می‌توان Strongly Typed دانست. برای مثال i32 نشانه یک عدد صحیح 32 بیتی و i32** نشانه اشاره‌گری (Pointer) به یک اشاره‌گر دیگر است که اشاره‌گر دوم به یک عدد صحیح 32 بیتی اشاره می‌کند. در این سیستم برخی جزئیات مربوط به معماری ماشین مورد استفاده به صورت انتزاعی از سر راه برداشته می‌شوند. مثلاً سیستم فراخوانی و جواب‌دهی ماشین، تنها با استفاده از دستورالعمل‌های call و ret با آرگومان‌های اجباری انتزاعی می‌شوند. تفاوت اساسی دیگری که میان LLVM IR و کدهای زبان ماشین، وجود دارد این است که LLVM IR از تعداد مشخصی ثبات نام‌گذاری‌ شده استفاده نمی‌کند، بلکه مجموعه‌ای نامتناهی از متغیرهای موقتی را به کار می‌برد که نام آن‌ها با علامت % شروع می‌شود.در پس پیاده‌سازی به صورت یک زبان، LLVM IR در واقع در سه فرم متناظر (Isomorphic) تعریف می‌شود. در بالاترین لایه قالب متنی (Textual Format) قرار دارد. در لایه میانی، ساختار داده درون حافظه (In-Memory Data Structure) وجود دارد که توسط بهینه‌سازها ایجاد و ویرایش می‌شود و در لایه زیرین یک قالب کارا و فشرده به صورت بیت‌کد (BitCode) روی دیسک وجود خواهد داشت. پروژه LLVM همچنین ابزارهایی را برای تبدیل فرمت نوشتاری دیسک از حالت متنی به حالت باینری فراهم می‌کند. برنامه llvm-as فایل‌های متنی با پسوند .ll را به فایلی با پسوند .bc تبدیل می‌کند که حاوی بیت‌کدها است و برنامه llvm-dis دقیقاً عکس این کار را انجام می‌دهد. نحوه نمایش میانی یا Intermediate Representation یک کامپایلر از این جهت حائز اهمیت است که می‌تواند دنیایی کامل برای Optimizer آن کامپایلر باشد؛ چرا‌که برخلاف Frontend و Backend کامپایلر، Optimizer به یک زبان خاص یا یک معماری سخت‌افزاری خاص محدود نیست. از طرف دیگر، این نمایش واسطه باید به‌خوبی به هر دو طرف خدمت‌رسانی کند. یعنی باید به گونه‌ای باشد که تولید کدهای آن برای Frontend ساده باشد و در عین حال به اندازه کافی گویا و توصیفی باشد که بهینه‌سازی‌های مهم و حیاتی را برای ماشین‌های مقصد مختلف امکان‌پذیر کند.

    نوشتن یک بهینه‌ساز‌ برای LLVM IR
    برای داشتن درکی ساده از این‌که بهینه‌سازی چگونه کار می‌کند، بهتر است از چندین مثال استفاده کنیم. انواع مختلف و بسیار متفاوتی از بهینه‌سازی در سطح کامپایلر وجود دارد، بنابراین تجویز نسخه‌ای قطعی برای هر مشکل دلخواهی بسیار دشوار است. با این حال، بیشتر بهینه‌سازی‌ها از ساختاری سه مرحله‌ای به این شرح استفاده می‌کنند:
    به دنبال الگویی می‌گردند که قابل تبدیل باشد.
    کنترل می‌کنند که آیا این تبدیل در مورد این نمونه خاص صحیح و امن است. با به‌روزرسانی کد، تبدیل را انجام می‌دهند.ساده‌ترین نوع این بهینه‌سازی‌ها، تطبیق الگو روی شناسه‌های ریاضی است. به عنوان مثال، به ازای هر عدد صحیح x، حاصل x-x برابر صفر، x-0 برابر x و حاصل (x*2)-x برابر x خواهد بود. نخستین پرسش این است که چنین مواردی در LLVM IR چگونه نمایش داده می‌شوند؟ نمونه‌هایی از این موارد را در فهرست 3 مشاهده می‌کنید.

    %example1 = sub i32 %a, %a
    %example2 = sub i32 %b, 0
    %tmp = mul i32 %c, 2
    %example3 = sub i32 %tmp, %c

    برای انجام این نوع از تبدیل‌های جزئی، LLVM رابطی را برای ساده‌سازی دستورالعمل‌ها فراهم کرده است که به‌عنوان ابزاری در سایر تبدیل‌های سطح بالا نیز مورد استفاده قرار می‌گیرد. این تبدیل‌های خاص در تابعی با‌عنوان SimplifySubInst نگه‌داری می‌شوند و همانند فهرست‌4 هستند.

    //
    X - 0 -> X
    if (match(Op1, m_Zero
    ((()
    return Op0
    ;
    //
    X - X -> 0
    if (Op0 == Op1
    return Constant::getNullValue(Op0->getType
    ;())
    //
    l(X*2) - X -> X
    if (match(Op0, m_Mul(m_Specific(Op1), m_ConstantInt
    (((()<2>
    return Op1

    return 0; // Nothing matched, return null to indicate no transformation


    در این کدها، Op0 و Op1 به عملوندهای سمت چپ و راست یک عمل تفریق اعداد صحیح اشاره می‌کنند. نکته مهم این است که این عملوندها لزوماً حاوی مقادیر ممیز شناور بر‌اساس استاندارد IEEE نیستند. خود LLVM براساس ++ C توسعه‌داده شده است که قابلیت‌های تشخیص و انطباق الگوی آن (در مقایسه با زبان‌های تابعی نظیر Objective Caml) چندان مشهور نیستند، اما این زبان یک سیستم قالب‌های بسیار کلی (Very General Template System) دارد که امکان پیاده‌سازی قابلیت‌هایی مشابه را فراهم می‌کند.
    تابع match و توابع با پیشوند m_ امکان انجام عملیات‌های انطباق الگوی توصیفی را روی کدهای LLVM IR فراهم می‌کنند. به عنوان مثال تابع m_Specific تنها تطابق‌هایی را می‌یابد که در آن‌ها عملوند سمت چپ یک عملیات ضرب همانند Op1 باشد.
    شكل 3: پیاده‌سازی LLVM از طراحی سه مرحله‌ای

    با استفاده از این سه مورد در کنار یکدیگر، تمام الگوها تطابق یافته و و تابع، جایگزینی را باز می‌گرداند یا در صورت نبود امکان جایگزینی یک اشاره‌گر خالی را باز می‌گرداند. فراخواننده این تابع (Simplify Instruction) یک توزیع‌کننده (Dispatcher) است که دستورالعمل‌ها را جابه‌جا کرده و آن‌ها را میان توابع کمکی توزیع می‌کند. این تابع توسط بهینه‌سازهای مختلف فراخوانده می‌شود. یک نمونه از این فراخوانی‌ها را می‌توانید در فهرست 5 ببینید.
    for (BasicBlock::iterator I = BB->begin(), E
    =م( BB->end(); I != E; ++I
    if (Value *V = SimplifyInstruction(I))
    م( I->replaceAllUsesWith(V

    این کد به‌سادگی تمام دستورالعمل‌های یک بلوک را در یک حلقه بررسی می‌کند تا ببیند آیا هیچ یک از آن‌ها قابل بهینه‌سازی هست یا نه. اگر پاسخ مثبت باشد، یعنی Simplify Instruction مقداری غیر صفر را بازگرداند، از replace All Uses With برای به‌روزرسانی کد و تغییر عملیات قابل ساده‌سازی استفاده می‌کند.

    پیاده‌سازی LLVM از طراحی سه مرحله‌ای
    در تمام کامپایلرهای مبتنی بر LLVM، قسمت Frontend وظیفه parse کردن، صحت‌سنجی و یافتن خطاها در کد ورودی و سپس ترجمه کد parse شده به LLVM IR را بر عهده دارد. ترجمه کد به LLVM IR معمولاً (و نه همیشه) از طریق ایجاد یک AST و سپس تبدیل آن به LLVM IR انجام می‌گیرد. این کد IR در صورت نیاز ممکن است چندین بار برای تحلیل و بهینه‌سازی بررسی شود که این کار باعث بهبود کد خواهد شد. این کد، همان‌طور که در شکل 3 می‌بینیم، پس از آن به مولد کد داده می‌شود تا کدهای محلی زبان ماشین را تولید کند. این یک پیاده‌سازی بسیار سرراست از طراحی سه‌مرحله‌ای است، اما برخی توانایی‌ها و انعطاف‌پذیری‌هایی که معماری LLVM به‌واسطه LLVM IR می‌تواند کسب کند، در این حالت از دست خواهند رفت. (شکل 3)

    پیاده‌سازی LLVM از طراحی سه مرحله‌ای
    LLVM IR نمایشی کامل از کد (Complete Code Representation)
    LLVM IR هم به خوبی تعریف شده است و هم تنها رابط موجود به Optimizer است. به این معنا که برای نوشتن یک Frontend برای LLVM تنها چیزی که باید بدانید این است که LLVM IR چیست، چگونه کار می‌کند و انتظار استفاده از چه ثابت‌هایی را دارد. به این دلیل که LLVM IR فرمی متنی از رده اول دارد، ممکن و منطقی است که Frontend ایجاد شده توسط شما خروجی LLVM IR خود را به صورت متنی تولید کند و سپس از پایپ‌های یونیکس برای ارسال آن به مراحل بعدی Optimizer و مولد کد دلخواه خود استفاده کنید.
    هرچند ممکن است عجیب به نظر برسد، اما این قابلیت در واقع یک ویژگی نوین در LLVM است و یکی از دلایل اصلی موفقیت و کاربردهای گسترده LLVM به شمار می‌رود. حتی کامپایلر به شدت موفق GCC که معماری بسیار خوبی هم دارد، از چنین ویژگی سودمندی برخوردار نیست. مدل نمایش میانی GCC یعنی GIMPLE یک نمایش کاملاً مستقل و کامل نیست. به عنوان مثال، هنگامی که مولد کد GCC می‌خواهد از اطلاعات دیباگ DWARF صرف نظر کند، به عقب باز می‌گردد و فرم «درختی» کد منبع را بررسی می‌کند. خود GIMPLE برای نمایش عملیات‌ها در کد از Tuple استفاده می‌کند، اما (حداقل در GCC نسخه 4,5) برای نمایش عملوندها، ارجاع‌هایی به فرم درختی در سطح کد منبع را مورد استفاده قرار می‌دهد.چنین پیاده‌سازی‌ای به این معنا است که برنامه‌نویسان برای ایجاد Frontend باید ساختار داده درختی GCC را بشناسند و در کنار GIMPLE این ساختار درختی را نیز تولید کنند. قسمت‌های Backend در GCC نیز از همین مشکل رنج می‌برند، به همین دلیل نویسندگان Backend هم باید بدانند که Backendهای RTL چگونه کار می‌کنند. در‌نهایت GCC راهی برای نوشتن GIMPLE یا هر نوع نمایش دیگری از کد به‌صورت متنی را ندارد. نتیجه این است که کسب تجربه با GCC به نسبت دشوار است و به همین دلیل Frontendهای محدودی دارد.در قسمت بعدي اين مقاله مزيت‌هايي كه كتابخانه‌هاي LLVM به ارمغان خواهند آورد، تعريف مولدهاي كد و فايل توصيف معماري مقصد براي LLVM مورد بررسي قرار خواهند گرفت و در نهايت خواهيم ديد كه طراحي ماجولار LLVM چه مزياتي را به‌همراه خواهد داشت.

    نردبان این جهان ما و منیست
    عاقبت این
    نردبان افتادنیست
    لاجرم آن کس که بالاتر نشست
    استخوانش سخت تر خواهد شکست




    #2 ارسال شده در تاريخ 11th November 2012 در ساعت 10:29

  3. *Mohammad* آواتار ها
    *Mohammad*
    مدیر سابق
    May 2011
    63,336
    22,637
    تشکر شده : 91,444

    پیش فرض

    LLVM مجموعه‌ای از کتابخانه‌ها است
    بعد از موضوعات مرتبط با طراحی LLVM IR، مهم‌ترین جنبه آن این است که LLVM به صورت مجموعه‌ای از کتابخانه‌ها طراحی شده است. این درست برخلاف رویکردی است که در کامپایلر خط فرمان GCC یا در ماشین‌های مجازی JVM یا دات‌نت دیده می‌شود. پروژه LLVM در واقع یک زیرساخت است؛ مجموعه‌ای از فناوری‌های مفید کامپایلر است که می‌توانند برای حل مسایل و مشکلات خاص (مثلاً ساخت یک کامپایلر C یا یک Optimizer در پایپ‌لاین جلوه‌های ویژه) مورد استفاده قرار بگیرند. این موضوع در حالی که یکی از قدرتمندترین ویژگی‌های LLVM است، یکی از نکات طراحی آن است که بسیار کمتر از بقیه شناخته شده است.
    بیایید به عنوان نمونه، به طراحی یک Optimizer نگاهی بیاندازیم. این Optimizer کدهای LLVM IR را خوانده، آن را کمی دستکاری کرده و یک کد LLVM IR جدید تولید می‌کند که به نظر می‌رسد سریع‌تر اجرا می‌شود. در LLVM نیز همانند بسیاری از کامپایلرهای دیگر، Optimizer به صورت پایپ‌لاینی از چندین دور(Pass) اجرای مجزای توابع بهینه‌سازی طراحی شده است که هریک به صورت جداگانه روی کد ورودی اجرا شده و امکان اعمال برخی تغییرات
    را دارند. بررسی‌های خطی (Inliner)، نسبت‌دهی دوباره عبارت‌ها (Expression Reassociation) و جابه‌جایی کدهای نامتغیر حلقه‌ها، نمونه‌هایی معمول از این دورهای بررسی کد هستند. بسته به سطح بهینه‌سازی موردنظر، تعداد متفاوتی از دورهای بهینه‌سازی مورد استفاده قرار می‌گیرد. برای مثال کامپایلر Clang، با سوئیچ -O0 حتی یک دور بهینه‌سازی را هم اجرا نمی‌کند، اما با سوئیچ –O3 تعداد دورهای مورد استفاده بهینه‌سازی به 67 دور می‌رسد (با نسخه 8/2 LLVM). هر یک دور LLVM به صورت یک کلاس C++ نوشته شده است که به صورت غیرمستقیم از کلاس Pass مشتق می‌شود. بیشتر این دورها در یک فایل واحد با پسوند .cpp نوشته شده‌اند و زیرکلاس‌های آن‌ها که از Pass مشتق می‌شوند، در یک فضای نام (namespace) بدون اسم تعریف شده‌اند. این امر باعث می‌شود که این کلاس‌ها private بوده و تنها در فایل تعریف‌کننده‌شان قابل استفاده باشند. برای این‌که یک دور بهینه‌سازی مفید باشد، کدهایی در بیرون فایل آن هم باید بتوانند به آن دسترسی داشته باشند. به همین دلیل از هر فایل تنها یک تابع (برای ایجاد آن دور) export می‌شود. در فهرست‌۱ نمونه‌ای ساده شده از یک دور (نمونه اي از تعريف يك دور از بهينه سازي كد)را مشاهده می‌کنید که این توضیحات را آشکارتر می‌کند.


    namespace
    }
    class Hello : public FunctionPass
    }
    // : public
    Print out the names of functions in the LLVM IR being optimized
    (virtual bool runOnFunction(Function &F)
    ; ”cerr << “Hello: “ << F.getName() << “\n
    ;return false;
    {
    ;{
    {

    FunctionPass *createHelloPass() { return new Hello
    l;{()

    همان‌گونه که گفته شد، Optimizer طراحی شده برای LLVM، دورهای متعدد و متفاوتی را فراهم می‌کند که همه آن‌ها در قالب یکسانی نوشته شده‌اند. این دورها در قالب یک یا چند فایل با پسوند.o ترکیب شده و سپس در مجموعه‌ای از کتابخانه‌های آرشیوی (فایل‌هایی با پسوند .a در یونیکس) ذخیره می‌شوند. این کتابخانه‌ها تمام قابلیت‌های تحلیلی و تبدیلی را فراهم می‌آورد و این دورها کمترین وابستگی و ارتباط را با یکدیگر دارند. انتظار می‌رود که این دورها مستقل از یکدیگر کار کنند یا وابستگی‌هایشان به سایر دورها را (در صورت وجود) به صورت صریح بیان کنند. وقتی تعداد دورهای موردنظر برای اجرا مشخص شده باشد، PassManager در LLVM از اطلاعات صریح وابستگی‌ها برای برآوردن نیازهای هر یک از دورها استفاده کرده و اجرای آن‌ها را بهینه می‌کند.
    کتابخانه‌ها و قابلیت‌های تجریدی و انتزاعی بسیار عالی هستند، اما در واقع به حل مشکلات کمک نمی‌کنند. نکته جالب توجه زمانی آشکار می‌شود که کسی بخواهد ابزار جدیدی تولید کند که از فناوری کامپایلر استفاده می‌کند. مثلاً شاید کسی قصد ساخت یک کامپایلر JIT را برای یک زبان پردازش تصویر داشته باشد. سازنده چنین کامپایلری محدودیت‌هایی را در ذهن خود دارد. به عنوان مثال، شاید این زبان پردازش تصویر، به شدت نسبت به تأخیرهای زمان کامپایل حساس باشد یا ویژگی‌هایی در این زبان وجود داشته باشد که بهینه‌سازی آن‌ها برای افزایش کارایی از اهمیت زیادی برخوردار باشد. طراحی کتابخانه محور Optimizer در کامپایلر LLVM، این امکان را برای سازنده این زبان فراهم‌می‌آورد که هم دورهای مورد نظر برای پردازش تصویر و هم ترتیب اجرای آن‌ها را انتخاب کند. اگر همه چیز به صورت یک تابع منفرد و بزرگ نوشته شده بود، صرف وقت برای استفاده دوباره از بخش‌های مختلف آن در طراحی کامپایلر این زبان پردازش تصویر فرضی، چندان منطقی نبود. اگر تعداد اشاره‌گرها اندک باشد، تحلیل aliasها و بهینه‌سازی حافظه ارزش چندانی نخواهد داشت. هرچند به‌رغم تمام تلاش‌های ما، LLVM نمی‌تواند به صورتی جادویی تمام مشکلات مربوط به بهینه‌سازی را حل کند.
    چون در LLVM زیرسیستم مربوط به دورهای بهینه‌سازی ماجولار است و خود PassManager چیزی درباره سازوکار درونی دورهای بهینه‌سازی نمی‌داند، برنامه‌نویسان آزادند که دورهای خاص زبان موردنظر خود را طراحی کنند تا ضعف‌های موجود در دورهای بهینه‌سازی LLVM را پوشش داده یا فرصت انجام بهینه‌سازی‌های خاص زبان موردنظرشان را داشته باشند. شکل ۱ نمونه ساده‌ای از سیستم پردازش تصویر فرضی ما را نشان می‌دهد.
    زمانی که مجموعه بهینه‌سازی‌های موردنظر انتخاب شدند (و تصمیمات مشابه مربوط به مولد کد نیز گرفته شدند)، کامپایلر مربوط به پردازش تصویر به صورت یک فایل اجرایی یا یک کتابخانه داینامیک ایجاد می‌شود. با توجه به این‌که تنها ارجاع به دورهای بهینه‌سازی LLVM، توابع ساده‌ای هستند که به فایل‌های با پسوند .o اشاره می‌کنند و با توجه به این‌که بهینه‌سازها در فایل‌هایی با پسوند .a ذخیره شده‌اند، تنها دورهایی از بهینه‌سازی که «مورد استفاده قرار گرفته‌اند» به برنامه نهایی متصل خواهد شد و نه کل Optimizer طراحی شده برای LLVM. در مثال قبلی ما (شکل ۱) به دلیل ارجاع برنامه به دورهای PassA و PassB این دورها به برنامه نهایی لینک می‌شوند. با توجه به این‌که دور PassB برای انجام برخی تحلیل‌ها از دور PassD هم استفاده می‌کند، PassD هم لینک می‌شود. اما به عنوان مثال دور PassC (و بسیاری دورهای بهینه‌سازی دیگر) مورد استفاده قرار نگرفته است و به همین دلیل کدهای آن به برنامه پردازش تصویر فرضی ما لینک نمی‌شود.
    این همان جایی است که طراحی کتابخانه محور LLVM قدرت خود را به نمایش می‌گذارد. این رویکرد طراحی سرراست، به LLVM اجازه می‌دهد که قابلیت‌های بسیار متنوعی را عرضه کند که برخی از آن‌ها تنها برای مخاطبانی خاص کاربرد خواهد داشت و این قابلیت‌ها موجب دردسر کسانی که تنها به قابلیت‌های ابتدایی کامپایلر احتیاج دارند، نخواهد شد. در نقطه مقابل، Optimizerهای کامپایلرهای سنتی به صورت حجم انبوهی از کدها یکپارچه با ارتباطات درونی ایجاد می‌شوند که جدا کردن بخش یا زیرمجموعه خاصی از آن‌ها، درک کردن سیستم کارشان و راه افتادن در استفاده از آن‌ها را بسیار دشوار می‌کند. به کمک LLVM می‌توانید سازوکار هر یک از Optimizerها را جداگانه درک کنید؛ بدون این‌که نیاز باشد نحوه عملکرد و کنار هم قرار گرفتن کل سیستم را بفهمید. همچنین این طراحی کتابخانه محور، دلیلی است که نشان می دهد چرا برخی افراد درک نادرستی از چیستی LLVM دارند.
    کتابخانه‌های LLVM قابلیت‌های فراوانی دارند، اما آن‌ها به خودی خود هیچ کاری انجام نمی‌دهند. این وظیفه طراح برنامه کلاینت این کتابخانه‌ها (مثلا کامپایلر Clang برای زبان C) است که تعیین کند برای بهترین استفاده، این قطعات چگونه باید در کنار هم قرار بگیرند. این لایه‌بندی محتاطانه، factoring و تمرکز روی قابلیت‌های زیرمجموعه‌ها باعث می‌شود که بتوان Optimizer پروژه LLVM را در محیط‌های متنوع و برای کاربردهای بسیار متفاوت مورد استفاده قرار داد. همچنین باید به خاطر داشت که صرف فراهم شدن قابلیت‌های کامپایل JIT توسط LLVM به این معنی نیست که همه کلاینت‌ها باید از LLVM استفاده کنند.

    نردبان این جهان ما و منیست
    عاقبت این
    نردبان افتادنیست
    لاجرم آن کس که بالاتر نشست
    استخوانش سخت تر خواهد شکست




    #3 ارسال شده در تاريخ 11th November 2012 در ساعت 10:29

  4. *Mohammad* آواتار ها
    *Mohammad*
    مدیر سابق
    May 2011
    63,336
    22,637
    تشکر شده : 91,444

    پیش فرض

    طراحي مولد کد LLVM برای هدف گرفتن ماشین‌های مختلف
    مولد کد LLVM وظیفه تبدیل LLVM IR به کدهای زبان ماشین معماری مورد نظر را بر عهده دارد. از یک سو این مولد کد باید بهترین کد زبان ماشین ممکن را برای هر معماری مقصد دلخواهی تولید کند (در حالت ایده‌آل هر مولد کد باید به‌صورت اختصاصی برای کدهای ماشین مقصد نوشته شده باشد) و از سوی دیگر، مولدهای کد برای تمام ماشین‌های مقصد باید مشکلات مشابهی را حل کنند. برای نمونه، تمام مولدهای کد باید مقادیری را به رجیسترها نسبت دهند و اگرچه هر معماری مقصد فایل‌های رجیستر جداگانه و متفاوتی دارد، اما الگوریتم‌های استفاده شده برای معماری‌های مقصد متفاوت باید تا حد ممکن مشابه و قابل استفاده دوباره باشند.

    شكل 1:سیستم پردازش تصویر فرضی XYZ با استفاده از LLVM
    با رویکردی مشابه آن‌چه در بخش Optimizer دیدیم، مولد کد LLVM نیز مشکل تولید کد را با تفکیک آن به چند دور (Pass) مختلف حل کرده و تعدادی از دورهای درونی را تعریف می‌کند که به صورت پیش‌فرض انجام می‌شوند. انتخاب دستورالعمل، اختصاص رجیسترهای حافظه، زمان‌بندی، بهینه‌سازی چیدمان کد و انتشار اسمبلی از جمله این دورها هستند.
    به این ترتیب، برنامه‌نویس Backend این شانس را خواهد داشت که دورهای موردنظر خود را از میان دورهای پیش‌فرض انتخاب کند، آن‌ها را بازنویسی کرده یا بسته به نیاز دورهایی کاملا اختصاصی را برای معماری موردنظر خود پیاده‌سازی کند. به عنوان مثال، Backend مربوط به معماری x86 به دلیل کم بودن تعداد رجیسترهای این معماری، از یک سیستم زمان‌بندی برای کاهش فشار روی رجیسترها (Register-Pressure-Reducing) استفاده می‌کند، اما Backend مربوط به معماری PowerPC به دلیل زیاد بودن تعداد رجیسترها از یک سیستم زمان‌بندی برای بهینه‌سازی تأخیر استفاده می‌کند. همچنین Backend مربوط به معماری x86 از یک دور اختصاصی استفاده می‌کند که پشته ممیز شناور x87 را مدیریت می‌کند و Backend مربوط به معماری ARM از دوری اختصاصی استفاده می‌کند که Constant Pool Island را در مکان‌های موردنیاز در درون تابع‌ها قرار می‌دهد. این انعطاف‌پذیری امکان تولید کدهای عالی را برای برنامه‌نویسان Backend فراهم می‌کند، بدون این‌که نیازی به نوشتن یک مولد کد جدید برای معماری مقصد وجود داشته باشد.

    فایل‌های توصیف مقصد در LLVM
    رویکرد ترکیب و تطبیق (Mix and Match) به برنامه‌نویسان Backend امکان می‌دهد که مواردی را که برای معماری موردنظرشان لازم به نظر می‌رسد، انتخاب کنند و هنگام انتقال به معماری‌های متفاوت و جدید، امکان استفاده دوباره از بسیاری از کدهای نوشته شده را فراهم می‌آورد. این امر چالش دیگری را به وجود خواهد آورد. هر بخش کد مشترک میان معماری‌های سخت‌افزاری مختلف باید بتواند با سیستمی عمومی و فراگیر، درباره خصوصیات و ویژگی‌های معماری مقصد اطلاعاتی کسب کند. برای مثال یک تخصیص‌دهنده رجیستر مشترک باید بداند که فایل رجیستر معماری مقصد کدام است و برای دستورالعمل‌ها و عملیات متناظری که روی رجیسترهای حافظه انجام می‌شود، چه محدودیت‌هایی وجود دارد.
    راه‌حل LLVM برای این مشکل، ارائه مشخصات معماری مقصد در قالب زبانی اختصاصی و توصیفی است که در مجموعه‌ای از فایل‌های با پسوند .td ذخیره شده و توسط ابزار tblgen مورد پردازش قرار می‌گیرند. نمونه‌ای ساده‌شده از فرآیند ایجاد کد مقصد برای معماری x86 در شکل۲ قابل مشاهده است. زیرسیستم‌های متفاوتی که توسط فایل‌های .td پشتیبانی می‌شوند، این امکان را برای توسعه‌دهندگان Backend فراهم می‌آورد که بخش‌های متنوعی از معماری سخت‌افزاری موردنظرشان را ایجاد و پیاده‌سازی کنند. به عنوان مثال، Backend مربوط به معماری x86 یک کلاس رجیستر را تعریف می‌کند که وظیفه آن نگه‌داری از کلیه رجیسترهای 32بیتی معماری x86 است. این کلاس را که GR32 (در فایل‌های .td تمام تعاریف مربوط به معماری مقصد با حروف بزرگ نوشته می‌شوند) نامیده می‌شود، می‌توانید در فهرست ۲ مشاهده کنید.
    def GR32 : RegisterClass<[i32], 32
    , [EAX, ECX, EDX, ESI, EDI, EBX, EBP, ESP
    {...} R8D, R9D, R10D, R11D, R14D, R15D, R12D, R13D]>

    فهرست 2:تعریف کلاس GR32 برای معماری x86

    این تعریف مشخص می‌کند که رجیسترهای معرفی شده در این کلاس می‌توانند مقادیر عددی صحیح 32بیتی (i32) را نگه‌داری کنند و ترجیحاً باید به صورت 32 بیتی انطباق داده شوند. 16 رجیستر موجود با نام معرفی شده‌اند (که توصیف آن‌ها در بخش دیگری از فایل‌های .td درج شده است) و اطلاعات دیگری نیز وجود دارد که ترتیب ترجیحی تخصیص این رجیسترها و سایر موارد مرتبط را توضیح می‌دهند. با در دسترس بودن این توصیف، دستورالعمل‌های خاص می‌توانند به آن مراجعه کرده و از آن به عنوان یک عملوند استفاده کنند. برای مثال دستورالعمل ”complement a 32bit register” همانند فهرست ۳ خواهد بود.


    let Constraints = “$src = $dst” in
    def NOT32r : I<0xF7, MRM2r
    (outs GR32:$dst), (ins GR32:$src)
    “not{l}\t$dst”
    <[( [(set GR32:$dst, (not GR32:$src)
    فهرست 3:پیاده‌سازی دستورالعمل complement a 32bit register
    این توصیف تعیین می‌کند که NOT32r یک دستورالعمل است (از کلاس I tblgen استفاده می‌کند)، اطلاعات Encoding را فراهم کرده (0xF7 , MRM2r)، مشخص می‌کند که قصد استفاده از $dst (یک رجیستر 32بیتی خروجی) و $src (یک رجیستر 32بیتی ورودی) را دارد. در این تعریف اشاره به کلاس GR32 رجیسترها تعیین می‌کند که استفاده از کدام رجیسترها مجاز خواهد بود. همچنین قواعد نحوی مورد استفاده برای اسمبلی این دستورالعمل (هم با قالب اینتلی و هم با قالب AT&T) را مشخص کرده، اثر این دستورالعمل و الگویی که باید در خط آخر با آن منطبق شود را تعریف می‌کند. کلمه کلید let در سطر نخست به تخصیص‌دهنده رجیستر‌ها می‌گوید که رجیستر ورودی و خروجی باید به یک رجیستر فیزیکی واحد نسبت داده شوند.
    این تعریف، توصیفی فشرده از دستورالعمل است و کدهای معمول LLVM می‌توانند به کمک tblgen اطلاعات بسیار زیادی را از آن استخراج کنند. کامپایلر برای انتخاب دستورالعمل مناسب براساس تطابق الگوها در کد ورودی IR، تنها به همین توصیف نیاز دارد. این توصیف به تخصیص‌دهنده رجیسترها می‌گوید که چگونه کارها را پردازش کند و برای تبدیل کردن دستورالعمل‌ها به کدهای زبان ماشین (و بالعکس) کافی خواهد بود. همچنین برای parse کردن و درج این دستورالعمل‌ها به صورت متنی نیز کفایت خواهد کرد. این قابلیت‌ها امکان پشتیبانی از ایجاد یک اسمبلر مستقل و منفرد برای معماری x86 را فراهم می‌کند. این اسمبلر به سادگی می‌تواند جایگزین gas (سرنام GNU assembler) شود. علاوه بر این، ایجاد دیس‌اسمبلر و حتی مدیریت دستورالعمل‌ها برای کامپایلرهای JIT نیز از‌طریق این توصیف‌ها ممکن خواهد بود.در کنار فراهم‌آوردن ویژگی‌های مفید، در اختیار داشتن قابلیت تولید اطلاعات مختلف از یک «حقیقت» واحد به دلایل دیگری نیز مفید خواهد بود. چنین رویکردی بروز تعارض میان اسمبلر و دیس‌اسمبلر بر سر قواعد نحوی (syntax) یا کدگذاری باینری را تقریباً غیرممکن می‌کند. همچنین توصیف‌های معماری مقصد را کاملاً آزمون‌پذیر می‌سازد. کدگذاری‌های دستورالعمل‌ها را می‌توان با Unit Testing و آن هم بدون درگیر کردن کل مولد کد کنترل کرد. با این‌که تلاش بر این است که بیشترین اطلاعات ممکن از یک معماری مقصد خاص در قالبی توصیفی در فایل‌های .td آورده شود، اما در هر صورت ما همه اطلاعات را در اختیار نخواهیم داشت. در عوض نویسندگان Backend باید برای برخی روال‌های پشتیبانی به نوشتن کدهای C++ پرداخته و دورهای اختصاصی معماری موردنظرشان را پیاده‌سازی کنند. هرچه تعداد معماری‌های مقصد پشتیبانی شده توسط LLVM بیشتر می‌شود، افزایش میزان اطلاعات توصیفی از معماری سخت‌افزاری که در فایل‌های .td گنجانده می‌شوند، اهمیت بیشتری می‌یابد. ما برای برطرف کردن این مشکل، در حال افزایش قابلیت‌های توصیفی فایل‌های .td هستیم. مزیت این امر این است که نوشتن Backend برای معماری‌های متفاوت به‌مرور زمان ساده‌تر و ساده‌تر می‌شود.

    نردبان این جهان ما و منیست
    عاقبت این
    نردبان افتادنیست
    لاجرم آن کس که بالاتر نشست
    استخوانش سخت تر خواهد شکست




    #4 ارسال شده در تاريخ 11th November 2012 در ساعت 10:30

  5. *Mohammad* آواتار ها
    *Mohammad*
    مدیر سابق
    May 2011
    63,336
    22,637
    تشکر شده : 91,444

    پیش فرض

    ویژگی‌های جذابی که از طراحی ماجولار نشأت می‌گیرند
    ماجولار بودن علاوه‌بر این‌که ایده‌ای عالی در طراحی به شمار می‌رود، کتابخانه‌های فراوانی را با ویژگی‌های جذاب در اختیار کلاینت‌های LLVM قرار می‌دهد. این قابلیت‌ها از این موضوع نشات می‌گیرند که LLVM تنها برخی عملکردها را فراهم می‌کند اما تنظیم و تعیین بیشتر سیاست‌ها در زمینه استفاده از این عملکردها را بر عهده کلاینت می‌گذارد. همان‌طور که پیش‌تر نیز اشاره شد، کدهای LLVM IR را می‌توان به کمک سریال‌سازی (serialization) با بازدهی بالا به قالبی باینری به نام بیت‌کدهای LLVM تبدیل کرد.

    شكل 2:توصیف ساده‌شده معماری x86
    عکس این کار نیز به آسانی امکان‌پذیر است. با توجه به این‌که LLVM IR تمام اطلاعات را در خود دارد و فرآیند سریال‌سازی نیز باعث از دست رفتن اطلاعات نمی‌شود، می‌توان بخشی از عملیات کامپایل را به انجام رسانده، نتیجه را روی دیسک ذخیره‌ کرده و ادامه کار را در آینده از سر گرفت. این ویژگی تعدادی قابلیت جذاب را نیز به ارمغان می‌آورد که پشتیبانی از بهینه‌سازی در هنگام لینک کردن (Link-Time Optimization) و بهینه‌سازی در هنگام نصب‌کردن (Install-Time Optimization) از جمله آن‌ها هستند. هر دوی این ویژگی‌ها نیازمند به تعویق افتادن تولید کد از زمان کامپایل هستند. اگر کامپایلری به صورت سنتی تنها توانایی دیدن تنها یک واحد ترجمه (مثلاً یک فایل با پسوند .c و تمام سرآیندهای موردنیازش) در هر زمان را داشته باشد، دیگر قابلیت بهینه‌سازی کل فایل‌های یک پروژه در یک قالب واحد و در تطبیق با یکدیگر را نخواهد داشت. بهینه‌سازی در هنگام لینک کردن یا LTO می‌تواند این مشکل را حل ‌کند. کامپایلرهای مبتنی بر LLVM نظیر Clang می‌توانند با سوئیچ‌های خط فرمان –flto و –O4 از این قابلیت استفاده کنند.با استفاده از این سوئیچ‌ها، کامپایلر به جای نوشتن فایل‌های شیئی محلی (Native Object File) بیت‌کدهای LLVM را در فایل‌های.o ذخیره می‌کند و تولید کد مقصد را به زمان لینک کردن موکول می‌کند. این موضوع را می‌توانید در شکل۳ مشاهده کنید.
    شكل 3:بهینه‌سازی در هنگام لینک کردن یا LTO
    جزئیات بر اساس سیستم‌عامل مورد استفاده ممکن است متفاوت باشند، اما نکته مهم این است که برنامه linker درک می‌کند که در فایل‌های .o به جای اشیای محلی، بیت‌کدهای LLVM ذخیره شده‌اند. در صورت مواجه شدن با چنین وضعیتی، linker محتوای تمام فایل‌ها را به حافظه منتقل کرده، آن‌ها را به هم لینک کرده و در نهایت بهینه‌ساز LLVM را روی کل کد حاصل اجرا می‌کند. چون در این وضعیت Optimizer بخش بزرگ‌تری از کد را می‌بیند، بهینه‌سازی‌های کامل‌تری (نظیر حذف کدهای زاید، inline کردن، استفاده از مقادیر ثابت بیشتر) را می‌تواند به انجام برساند. اگرچه بیشتر کامپایلرهای مدرن از LTO پشتیبانی می‌کنند، بیشتر آن‌ها (از جمله GCC، Open64 و کامپایلر اینتل) این کار را با یک فرآیند سنگین و کند سریال‌سازی انجام می‌دهند. در LLVM قابلیت LTO به‌صورت مستقیم از طراحی سیستم نشات می‌گیرد و (برخلاف بسیاری از کامپایلرهای دیگر) روی گستره وسیعی از زبان‌های برنامه‌نویسی کار خواهد کرد؛ چرا‌که IR واقعاً مستقل از زبان برنامه‌نویسی مبدأ است. ایده بهینه‌سازی در هنگام نصب، تولید کد را حتی از هنگام لینک کردن نیز عقب‌تر برده و آن را تا زمان نصب برنامه به تأخیر می‌اندازد. نحوه کار این سیستم را می‌توانید در شکل ۴ ببینید. زمان نصب، به خصوص هنگامی که برنامه شما به صورت یک پکیج عرضه شود یا روی یک دستگاه قابل حمل دانلود یا آپلود شود، زمان بسیار جذابی است. زمان نصب هنگامی است که شما تازه مشخصات دستگاه مقصد را می‌یابید.
    مثلاً در سخت‌افزارهای خانواده x86 تنوع فراوانی از تراشه‌ها و خصوصیت‌های سخت‌افزاری وجود خواهد داشت. با به تعویق انداختن انتخاب دستورالعمل، زمان‌بندی و سایر جنبه‌های تولید کد، می‌توان بهترین حالت را برای سخت‌افزاری که قرار است برنامه را اجرا کند، انتخاب کرد. کامپایلرها بسیار پیچیده هستند و در آن‌ها کیفیت بسیار مهم است، به همین دلیل آزمایش آن‌ها امری حیاتی است. به عنوان مثال، پس از برطرف کردن اشکالی که باعث از کار افتادن Optimizer می‌شده است، یک آزمون رگرسیون نیز باید انجام شود تا اطمینان حاصل شود که این خطا دیگر رخ نخواهد داد. رویکرد سنتی آزمایش به این شکل است که فایلی مثلا با پسوند .c نوشته شده و به کامپایلر داده شود. سپس با سیستمی کنترل شود که آیا کامپایلر دچار اشکال خواهد شد یا خیر. این روشی است که برای کامپایلر GCC مورد استفاده قرار می‌گیرد. مشکل چنین رویکردی این است که کامپایلر از زیرسیستم‌های مختلفی تشکیل شده است و تعداد دورهایی که توسط Optimizer انجام می‌شود نیز بسیار زیاد هستند. در مثال قبلی، تمام این موارد ممکن است منبع بروز مشکل باشند یا پس از رفع باگ، دچار مشکلی جدید شوند. اگر چیزی در Frontend یا در Optimizer اولیه تغییر کند، یافتن منبع مشکل و نوشتن برنامه آزمون برای اطمینان از تصحیح کامل خطا به شدت دشوار خواهد‌بود.با استفاده از قالب متنی LLVM IR و یک Optimizer ماجولار، ابزارهای آزمودن LLVM شامل آزمون‌های رگرسیون هدف‌مندی است که می‌توانند LLVM IR را از روی دیسک خوانده، تنها یک دور از بهینه‌سازی را روی آن اجرا کرده و رفتار مورد انتظار از کامپایلر را بررسی کنند. فراتر از آزمون‌های مربوط به از کار افتادن کامپایلر، ممکن است یک آزمون بخواهد کنترل کند که آیا عملیات بهینه‌سازی واقعاً به انجام می‌رسد یا نه. در فهرست‌۴ می‌توانید نمونه‌ای از این آزمون‌ها را ببینید که انجام دور بهینه‌سازی propagation روی دستورالعمل add را کنترل می‌کند.

    ;
    RUN: opt < %s -constprop -S | FileCheck %s
    define i32 @test
    {()
    %A = add i32 4, 5
    ret i32 %A
    ;
    CHECK: @test()
    ;
    CHECK: ret i32 9
    {
    فهرست 4:کنترل اجرای بهینه‌سازی propagation روی دستورالعمل add

    خطی که با RUN شروع می‌شود، مشخص کننده دستوری است که باید اجرا شود. در این مورد دستورهای opt و FileCheck باید اجرا شوند. برنامه opt در واقع یک wrapper است که مدیر دورهای LLVM (سرنامLLVM Pass Manager) را در خود دارد و تمام دورهای استاندارد را لینک کرده و حتی امکان بارگذاری داینامیک پلاگین‌ها و سایر دورهای اختصاصی را داشته و آن‌ها را از طریق خط فرمان در اختیار کاربر قرار می‌دهد. ابزار FileCheck کنترل می‌کند که آیا ورودی استانداردش با یک سری از CHECKها مطابقت دارد یا خیر. در این مورد، آزمون ساده ما کنترل می‌کند که آیا دور constprop دستورالعمل جمع دو عدد 4 و 5 را با عدد ثابت 9 جایگزین می‌کند یا خیر.
    اگرچه این آزمون ممکن است مثالی ابتدایی و کم اهمیت به نظر برسد، انجام همین کار از طریق نوشتن یک فایل .c بسیار دشوار خواهد بود. بخش‌های Frontend معمولاً این کار را در هنگام parse کردن کد انجام می‌دهند و به همین دلیل نوشتن کدی که بتواند از این مرحله گذر کرده و انجام این کار را در یکی از دورهای بهینه‌سازی کنترل کند، بسیار دشوار و حتی ناممکن خواهد بود. اما چون LLVM IR را می‌توان به صورت یک فایل متنی بارگذاری کرده، دورهای دلخواه بهینه‌سازی را روی آن به انجام رساند و نتیجه را به صورت یک فایل متنی دیگر دریافت کرد، آزمودن آن‌چه مورد نظر است بسیار سرراست و راحت خواهد بود. خواه در هنگام آزمودن ویژگی‌های جدید باشد یا در هنگام انجام آزمون‌های رگرسیون.زمانی که یک باگ در کامپایلر یا سایر کلاینت‌های LLVM مشاهده می‌شود، قدم اول برای اصلاح آن ایجاد یک نمونه آزمون موردی است که بتواند این خطا را بازتولید کند. زمانی که این نمونه آزمایشی ایجاد شد، بهتر است آن را به ساد‌ه‌ترین شکل ممکن درآورد و تا حد ممکن به دقت محل بروز خطا (مثلاً یک دور از بهینه‌سازی که باعث ایجاد خطا می‌شود) را کشف کرد. اگرچه شما به تدریج یاد خواهید گرفت که چگونه این کار را به انجام برسانید، اما انجام این کارها زمانی که کامپایلر کدهای غلطی تولید می‌کند، بسیار دشوارتر و زمان‌برتر از زمانی است که کامپایلر از‌کار‌افتاده و متوقف می‌شود. ابزار BugPoint با استفاده از سریال‌سازی IR و ماجولار بودن طراحی LLVM، این فرآیند را به صورت خودکار به انجام می‌رساند. به عنوان مثال با مشخص کردن یک فایل ورودی با پسوند.ll یا .bc و همین‌طور تعدادی از دورهای بهینه‌سازی که باعث از کار افتادن کامپایلر می‌شود، ابزار BugPoint ورودی را به یک مجموعه دستورالعمل کوچک محدود کرده و دوری از بهینه‌سازی که عامل بروز مشکل است را دقیقاً مشخص می‌کند. خروجی این ابزار، آزمون خلاصه‌شده و دستور opt موردنیاز برای بازتولید خطا خواهد بود. این ابزار عملیاتش را با استفاده از تکنیک‌هایی شبیه دیباگ دلتا (Delta Debugging) به انجام می‌رساند. با توجه به این‌که BugPoint ساختار LLVM IR را درک می‌کند، برخلاف دستور معمول delta، هیچ زمانی را برای ایجاد کدهای IR نامعتبر و آزمودن آن با Optimizer تلف نمی‌کند.

    نردبان این جهان ما و منیست
    عاقبت این
    نردبان افتادنیست
    لاجرم آن کس که بالاتر نشست
    استخوانش سخت تر خواهد شکست




    #5 ارسال شده در تاريخ 11th November 2012 در ساعت 10:30

  6. *Mohammad* آواتار ها
    *Mohammad*
    مدیر سابق
    May 2011
    63,336
    22,637
    تشکر شده : 91,444

    پیش فرض

    در این بخش به بررسی مزیات این طراحی ماجولار در قسمت بهینه‌سازی کد (Optimizer) و همین‌طور بخش مولد کد یا Backend کامپایلر خواهیم پرداخت و همین‌طور خواهیم دید که LLVM چگونه با استفاده از فایل‌های توصیف معماری مقصد، می‌تواند برای کامپایل برنامه‌ها روی گستره وسیعی از سخت‌افزارها به کار برده شود.

    شكل 4:بهینه‌سازی در هنگام نصب‌کردن یا ITO
    در یک مورد پیچیده‌تر که با خطای کامپایل روبرو می‌شود، می‌توان ورودی، اطلاعات مولد کد، دستوری که باید به فایل اجرایی منتقل شود و یک ارجاع برای خروجی را تعیین کرد. در این حالت BugPoint ابتدا تعیین می‌کند که خطا به Optimizer مربوط است یا به مولد کد. پس از آن به‌صورت متناوب کد ورودی را به دو بخش تقسیم می‌کند. یک بخش «سالم» و یک بخش «مشکل‌دار». با انجام دادن این کار به صورت متناوب، BugPoint به تدریج قسمت مشکل‌دار را کوچک‌تر و کوچک‌تر می‌کند تا بتواند به حداقل کد آزمون دست یابد.
    ابزار BugPoint ابزاری بسیار ساده است که به کمک LLVM توانسته است هزاران ساعت زمان را در فرآیندهای عیب‌یابی و تصحیح خطا صرفه‌جویی کند. هیچ کامپایلر اپن‌سورس دیگری چنین ابزار سودمندی را در اختیار ندارد، چرا که این ابزار به شدت به آن نحوه نمایش میانی (IR) تعریف‌شده توسط LLVM وابسته است.
    با همه این‌ها BugPoint هم کامل نیست و بازنویسی آن می‌تواند منافع زیادی در بر داشته باشد. تاریخ نوشته شدن این ابزار به سال 2002 باز می‌گردد و معمولاً تنها هنگامی به روز شده است که کسی با ایراد و خطایی روبه‌رو شده که با ابزارهای موجود قابل رفع و رجوع نبوده است. این ابزار بدون هیچ طراحی اولیه یا مالکی در طی زمان رشد کرده و ویژگی‌های جدیدی (نظیر دیباگ کردن JIT) را کسب کرده است. LLVM از ابتدا برای دسترسی به قابلیت‌هایی که تا‌کنون شرح داده شد، ماجولار طراحی نشده است. در ابتدا این موضوع مکانیسمی دفاعی بود چرا که مشخص بود ما نمی‌توانیم از ابتدا همه چیز را به صورت مرتب و صحیح پیاده‌سازی کنیم. مثلاً پایپ‌لاین دورهای بهینه‌سازی ماجولار به این دلیل ایجاد شدند که بتوانیم هر یک از دورها را از بقیه مجزا کنیم و به این ترتیب با ایجاد شدن هر پیاده‌سازی جدید از یکی از دورها، نسخه قدیمی را به راحتی کنار بگذاریم. من در بسیاری موارد گفته‌ام که هیچ یک از زیرسیستم‌های LLVM کاملاً خوب و بی‌نقص نخواهند بود، مگر این‌که حداقل یک‌بار از نو نوشته شوند. یکی دیگر از جنبه‌های مهم و زیرکانه LLVM (که در میان کلاینت‌های کتابخانه‌های LLVM بسیار بحث‌برانگیز است)، تلاش ما برای بازنگری در تصمیمات قدیمی و ایجاد تغییرات گسترده در APIهای برنامه، بدون در نظر گرفتن نگرانی‌های مربوط به سازگاری با نسخه‌های گذشته (Backward Compatibility) است.
    برای مثال ایجاد تغییرات گسترده در خود LLVM IR نیازمند به‌روز رسانی تمام دورهای بهینه‌سازی و در نتیجه اعمال تغییرات بسیار زیاد در APIهای ++C است. ما در برخی شرایط چنین کارهایی را انجام داده‌ایم و این کار به رغم دردسری که برای کلاینت‌ها به وجود می‌آورد، برای حفظ این روند سریع رشد و بهبود LLVM الزامی است. برای ساده‌تر کردن کار کلاینت‌های بیرونی و همین‌طور پشتیبانی از امکان اتصال به سایر زبان‌های برنامه‌نویسی، ما wrapperهای فراوانی را به زبان C برای بسیاری از APIها فراهم کرده‌ایم که به شدت پایدار هستند. همچنین نسخه‌های جدید LLVM به خواندن فایل‌های قدیمی .ll و .bc ادامه خواهند داد. با نگاه به آینده ما قصد داریم که LLVM را ماجولارتر کرده و امکان استفاده از زیرمجموعه‌های آن را ساده‌تر کنیم. مثلاً قسمت مولد کد هنوز تا حد زیادی یکپارچه است. در حال حاضر، نمی‌توان برخی زیرمجموعه‌های LLVM را بر‌اساس ویژگی‌های موردنظر ایجاد کرده و مورد استفاده قرار داد. به عنوان مثال، اگر شما بخواهید از JIT استفاده کنید اما به اسمبلی inline، مدیریت استثنا یا تولید اطلاعات دیباگ احتیاج نداشته باشید، باید بتوانید مولد کد را بدون تمام این قابلیت‌ها مورد استفاده قرار دهید (کاری که اکنون امکان‌پذیر نیست).
    ما همچنین به صورت پیوسته کیفیت کد تولید شده توسط Optimizer و مولد کد را بهبود داده‌ایم، قابلیت‌هایی را به IR افزوده‌ایم تا از زبان‌های جدید و معماری‌های مقصد جدید، بهتر پشتیبانی کند و امکان انجام بهینه‌سازی‌های سطح بالای مختص زبان‌های برنامه‌نویسی خاص در LLVM فراهم شود.پروژه LLVM به رشد خود ادامه خواهد داد و از جهات مختلف بهبود خواهد یافت. مشاهده روش‌های متفاوت استفاده از LLVM در سایر پروژه‌ها و همین‌طور این‌که در محیط‌های مختلف و جدیدی (که حتی به ذهن سازندگانش هم نمی‌رسید) کاربرد می‌یابد، بسیار خوشایند است. دیباگر جدید LLDB نمونه‌ای بارز از این موضوع است. این دیباگر از parserهای ++C ، C یا Objective-C موجود در Clang استفاده می‌کند تا بتواند عبارات را parse کند و از JIT موجود در LLVM برای ترجمه آن‌ها به کدهای مقصد استفاده می‌کند. سپس با استفاده از دیس‌اسمبلرهای LLVM و فایل‌های توصیف معماری مقصد به مدیریت نحوه انجام فراخوانی‌ها و امور دیگر می‌پردازد. امکان استفاده دوباره از کدهای موجود این پروژه، این فرصت را برای توسعه‌دهندگان دیباگرها فراهم می‌آورد تا تمرکز خود را به منطق دیباگر معطوف کنند نه این‌که دوباره گرفتار بازنویسی یک parser به زبان ++C شوند.
    به‌رغم موفقیت‌هایی که LLVM تاکنون داشته است، هنوز کارهای زیادی برای انجام باقی مانده است. هنوز این خطر هم وجود دارد که با گذشت زمان LLVM چابکی خود را از دست داده و گرفتار تحجر و خمودگی شود؛ هرچند هیچ راه‌حل جادویی برای این مشکل وجود ندارد. اما من امید دارم که قرار گرفتن مداوم در معرض چالش‌ها و مشکلات در حوزه‌های تازه، اعتقاد به بازنگری در تصمیم‌های قدیمی، طراحی مجدد و دور ریختن کدهای قدیمی، به حفظ این چابکی کمک کند. به هر حال هدف ما کامل بودن صددرصد نیست، بلکه بهتر شدن در گذر زمان است.

    نردبان این جهان ما و منیست
    عاقبت این
    نردبان افتادنیست
    لاجرم آن کس که بالاتر نشست
    استخوانش سخت تر خواهد شکست




    #6 ارسال شده در تاريخ 11th November 2012 در ساعت 10:31

موضوعات مشابه

  1. چاقي، عامل رفلكس معده است
    توسط AYS@N در انجمن گوارش
    پاسخ ها: 0
    آخرين نوشته: 25th December 2011, 01:54
  2. پاسخ ها: 0
    آخرين نوشته: 6th September 2010, 18:16
  3. زاغه‌نشينان بمبئي، سوژه تازه باليوود مي‌شوند
    توسط Totti forever در انجمن آرشیو سینما و تئاتر
    پاسخ ها: 0
    آخرين نوشته: 16th July 2010, 20:26
  4. مجيدي، فردوسي‌پور، استيلي و كاشاني پابه‌توپ شدند
    توسط ´•.MahSa.•` در انجمن بایگانی بخش ورزش
    پاسخ ها: 0
    آخرين نوشته: 5th August 2009, 17:17

علاقه مندی ها (Bookmarks)

مجوز های ارسال و ویرایش

  • شما نمیتوانید موضوع جدیدی ارسال کنید
  • شما امکان ارسال پاسخ را ندارید
  • شما نمیتوانید فایل پیوست کنید.
  • شما نمیتوانید پست های خود را ویرایش کنید
  •