PDA

View Full Version : دربارۀ JIT / Just-in-time compilation



eshpilen
یک شنبه 16 شهریور 1393, 08:04 صبح
JIT یا کامپایل دینامیک، کامپایل انجام شده در مدت اجرای یک برنامه است بجای قبل از اجرا. این اغلب متشکل از ترجمه به زبان ماشین است، که سپس بصورت مستقیم اجرا میشود، اما میتواند همچنین ترجمه به فرمت دیگری باشد.
JIT سرعت کد کامپایل شده را با انعطاف تفسیر ترکیب میکند، با یک سربار مفسر و سربار اضافه کامپایل (و نه فقط تفسیر).
کامپایلرهای JIT همچون مفسرها بصورت پیوسته ترجمه میکنند، اما کش شدن کد کامپایل شده تاخیر بر روی اجرای آیندهء کدهای یکسان در طول یک اجرای معین را به حداقل میرساند. از آنجاییکه فقط بخشی از برنامه کامپایل میشود، به میزان زیادی تاخیر کمتری نسبت به آنکه کل برنامه قبل از اجرا کامپایل میشد وجود دارد.
م: از این عبارات آخر متوجه میشیم که JIT کل برنامه رو به زبان ماشین کامپایل نمیکنه و بنابراین خودش هم در فرایند اجرا همواره دخالت داره و فقط بخشهای کامپایل شده از برنامه بصورت مستقیم روی CPU اجرا میشن.
در یک سیستم bytecode ای، کد منبع به یک حالت بینابین بنام bytecode ترجمه شده است. bytecode کد ماشین نیست، و میتواند بین معماری های رایانه قابل حمل باشد. bytecode میتواند بوسیلهء یک ماشین مجازی تفسیر یا روی آن اجرا شود (م: دقت کنید اینجا ظاهرا اجرا شدن روی یا تفسیر اشاره به دو حالت مختلف اجرای bytecode داره). کامپایلر JIT بایت کد را در بخشهای زیادی (یا به ندرت بطور کامل) میخواند و آنها را بصورت دینامیک به زبان ماشین کامپایل میکند تا برنامه بتواند سریعتر اجرا شود. جاوا بررسی های زمان اجرایی را بر روی بخشهای مختلفی از کد انجام میدهد و این دلیل آن است که تمام کد به یک باره کامپایل نمیشود. این میتواند بر هر فایل، بر هر تابع، یا حتی بر هر قطعه کد دلخواه انجام شود؛ کد میتواند هنگامی که در شرف اجرا شدن است کامپایل شود (نام just-in-time به همین خاطر است)، و سپس کش شده و بعدا بدون نیاز به کامپایل مجدد استفاده مجدد شود.
در مقابل، یک ماشین مجازی تفسیری به سادگی بایت کد را تفسیر خواهد کرد که عموما با پرفورمنس خیلی کمتری است. بعضی از مفسرها حتی کد منبع را تفسیر میکنند، بدون آنکه آن را ابتدا به بایت کد تبدیل کنند، که پرفورمنس آن حتی بدتر است.
یک هدف مشترک استفاده از تکنیک های JIT رسیدن به یا گذشتن از پرفورمنس کامپایل استاتیک است (م: توجه کنید که میگه JIT حتی میتونه از کامپایل استاتیک هم سرعت بیشتری داشته باشه)، در عین حالی که مزایای تفسیر بایت کد را حفظ میکند: بخش بزرگی از بار سنگین parse کردن کد منبع اصلی و انجام بهینه سازی پایه معمولا در زمان کامپایل، قبل از توزیع برنامه، انجام میشود: کامپایل از بایت کد به کد ماشین بسیار سریعتر از کامپایل از کد منبع است. بایت کد نصب شده بر خلاف کد native قابل حمل است. از آنجاییکه محیط اجرا بر روی کامپایل کنترل دارد، همچون بایت کد تفسیر شده، آن میتواند در یک sandbox امن اجرا شود. نوشتن کامپایلرهایی که بایت کد را به کد ماشین تبدیل میکنند ساده تر است، چون کامپایلر قابل حمل بایت کد قبلا بیشتر کار را انجام داده است.

کد JIT عموما پرفورمنس بسیار بهتری نسبت به مفسرها ارائه میکند. بعلاوه، آن میتواند در بعضی موارد پرفورمنس بهتری نسبت به کامپایل استاتیک ارائه کند، چون بسیاری از بهینه سازیها فقط در زمان اجرا ممکن هستند:

1- کامپایل میتواند برای CPU هدف و مدل سیستم عاملی که برنامه اجرا میشود بهینه شود . برای مثال JIT میتواند وقتی که کشف میکند که CPU از دستورات برداری SSE2 پشتیبانی میکند از آن دستورات استفاده کند. هرچند درحال حاضر هیچ سیستم JIT عمده ای که این را پیاده سازی کند وجود ندارد. برای بدست آوردن این سطح از بهینه سازی با یک کامپایلر استاتیک، شخص باید یا یک فایل اجرایی برای هر پلتفرم/معماری مورد نظر کامپایل کند، یا در غیر این صورت چندین نسخه از قسمت هایی از کد را در درون هر فایل اجرایی قرار دهد.

2- سیستم قادر است آمارهایی را راجع به اینکه برنامه واقعا چطور دارد در محیطی که در آن قرار دارد اجرا میشود جمع آوری کرده و میتواند برای پرفورمنس بهینه بازچینی (rearrange) و کامپایل مجدد کند. هرچند، بعضی از کامپایلرهای استاتیک همچنین میتوانند اطلاعات پروفایل را بعنوان ورودی دریافت کنند.

3- سیستم میتواند بهینه سازیهای کلی کد (برای مثال inline کردن توابع کتابخانه ای) را بدون از دست دادن مزایای لینک دینامیک و بدون بار اضافی ذاتی کامپایلرها و لینکرهای استاتیک انجام دهد. بویژه، موقع انجام جانشینی های کلی inline، یک فرایند کامپایل استاتیک ممکن است به بررسی های زمان اجرا و مطمئن شدن از اینکه یک فراخوانی virtual رخ خواهد داد اگر کلاس واقعی شیء متد inline شده را override کرده باشد، و بررسی شرط های محدوده بر روی دسترسی های آرایه در درون حلقه ها پردازش شوند نیاز داشته باشد. با کامپایل JIT در بسیاری از موارد این پردازش میتواند به خارج از حلقه ها منتقل شود که اغلب به افزایش سرعت های بزرگی منجر میشود.

4- هرچند این با زبانهای کامپایل استاتیک شونده مجهز به رفتگر (garbage collector) نیز ممکن است، یک سیستم بایت کد ساده تر میتواند کد اجرا شده را برای استفاده بهتر از کش (م: منظور حافظه کش CPU میباشد) بازچینی کند.

JIT معمولا موجب یک تاخیر ملایم در اجرای اولیهء یک برنامه میشود، بخاطر اینکه برای بارگذاری و کامپایل بایت کد زمان صرف میشود. عموما، هرچه JIT بهینه سازی بیشتری انجام دهد موجب تاخیر اولیهء بیشتری هم خواهد شد. بنابراین یک کامپایلر JIT مجبور است بین زمان کامپایل و کیفیت کدی که میخواهد تولید کند تعادل ایجاد نماید. هرچند، بنظر میرسد که بیشتر زمان شروع بخاطر عملیات I/O است تا کامپایل JIT (برای مثال، فایل داده کلاس rt.jar ماشین مجازی جاوا 40 مگابایت است و ماشین مجازی جاوا باید در این فایل بزرگ به دنبال مقدار زیادی داده بگردد).

یک بهینه سازی ممکن که توسط ماشین مجازی جاوای Sun بنام ماشین مجازی HotSpot استفاده میشود این است که تفسیر و کامپایل JIT را با هم ترکیب کنیم. کد برنامه ابتدا تفسیر میشود، اما ماشین مجازی جاوا توالی هایی از بایت کد را که بصورت مکرر اجرا میشوند تحت نظر میگیرد و آنها را برای اجرای مستقیم بر روی سخت افزار به کد ماشین ترجمه میکند. برای بایت کدی که تنها تعداد معدودی بار اجرا میشود، این زمان کامپایل را صرفه جویی میکند و تاخیر اولیه را کاهش میدهد؛ برای بایت کدی که بطور مکرر اجرا میشود، کامپایل JIT بعد از یک فاز اجرای کند اجرا میشود تا کد با سرعت بیشتری اجرا شود. بعلاوه، از آنجاییکه یک برنامه بیشتر زمان را در اجرای یک بخش کوچک از کد صرف میکند، کاهش زمان کامپایل قابل توجه است. دست آخر، در طول تفسیر اولیهء کد، آمارهایی درمورد اجرا میتوانند قبل از کامپایل جمع آوری شوند که به انجام بهتر بهینه سازی کمک میکند.

تعادل درست میتواند، بخاطر شرایط، متفاوت باشد. برای مثال، ماشین مجازی جاوای Sun دو حالت اصلی دارد: حالت کلاینت و حالت سرور. در حالت کلاینت، کامپایل و بهینه سازی حداقلی انجام میشود تا زمان شروع کاهش داده شود. در حالت سرور، کامپایل و بهینه سازی گسترده ای انجام میشود تا پرفورمنس یک بار که برنامه اجرا شد حداکثر باشد، اما این با قربانی کردن زمان شروع به کار اولیه انجام میشود. بقیهء کامپایلرهای JIT جاوا یک اندازه گیری زمان اجرا از تعداد بارهایی که یک متد اجرا شده است را در ترکیب با حجم بایت کد یک متد بعنوان یک پیشبینی برای تصمیم گرفتن درمورد اینکه چه زمانی اقدام به کامپایل کنند استفاده کرده اند. یکی دیگر، تعداد دفعات اجرا را در ترکیب با آشکارسازی حلقه ها استفاده میکند. در کل، پیشبینی دقیق آنکه کدام متدها را بهینه سازی کنیم، در برنامه هایی که برای مدت کوتاهی اجرا میشوند بسیار دشوارتر است تا برنامه هایی که برای مدت طولانی اجرا میشوند.

ابزار دیگری نام Ngen از میکروسافت، روش دیگری برای کاهش تاخیر اولیه است. Ngen بایت کد موجود در فایلهای اجرایی دات نت را پیشاپیش به کد ماشین native کامپایل میکند (پیشاپیش JIT میکند). در نتیجه، به کامپایل زمان اجرا نیازی نیست. فریمورک دات نت نسخهء 2.0 که با ویژوال استودیو 2005 توزیع شده بلافاصله بعد از نصب Ngen را روی همهء کتابخانه های DLL میکروسافت اجرا میکند. JIT کردن پیشاپیش راهی را برای بهبود زمان شروع اولیه فراهم میکند. اما، کیفیت کدی که آن تولید میکند ممکن است بقدر کدی که توسط JIT تولید شده است خوب نباشد، بخاطر دلیل یکسانی که چرا کدی که کامپایل استاتیک شده است، بدون بهینه سازی هدایت شده با پروفایل، نمیتواند بقدر کد کامپایل شده توسط JIT در خاص ترین حالات خوب باشد: کمبود دیتای پروفایل برای هدایت، بطور مثال، inline caching.

همچنین پیاده سازی های جاوایی وجود دارند که یک کامپایلر استاتیک را با یک کامپایلر JIT یا مفسر ترکیب میکنند.

منبع: بخشهایی از http://en.wikipedia.org/wiki/Just-in-time_compilation

eshpilen
یک شنبه 16 شهریور 1393, 08:13 صبح
راستی یه نکته ای که بنظرم رسید اینکه، ممکنه بعضیا بگن خب اگر پرفورمنس JIT اینقدر خوبه و ادعا میشه که حداقل در بعضی موارد و بعضی وقتا میتونه حتی از زبانهای کامپایل استاتیک مثل سی++ بهتر باشه، پس چرا تاحالا هیچوقت ندیدیم که مثلا یک برنامهء دات نت پرفورمنس بهتری از یک برنامهء سی++ داشته باشه.
باید بگم که بنظر میرسه که وقتی گفتن سرعت اجرا میتونه از سرعت اجرای کامپایل استاتیک (منظور کامپایل پیشاپیش است) بیشتر باشه، منظور کامپایل استاتیک برنامه ای در همون زبان بوده؛ یعنی با همون سطح برنامه نویسی و امکانات و سینتاکس. یعنی مثلا اگر برای زبان سی شارپ و فریمورک دات نت یک کامپایلر استاتیک درست کنیم که فایل اجرایی Native درست کنه. نه اینکه سرعت برنامه های نوشته شده در سی++ رو با سرعت برنامه های دات نت مقایسه کنیم؛ چون بهرحال سطح برنامه نویسی دات نت از سی++ بالاتره و اگر همون سینتاکس و امکانات هلو برو توی گلو رو در سی++ هم داشتیم و با این حال بازم سرعت برنامه های برتری همیشگی بر سرعت برنامه های دات نت داشت اونوقت این مقایسه درست بود، ولی وقتی شما در سی++ با سطح پایین تر برنامه مینویسید و طبیعتا بقدر برنامه نویسی دات نت و سی شارپ راحت و سطح بالا و امن (از نظر حفره های امنیتی و باگها و چکهای صحت ساختارها و دستورات برنامه و مدیریت خودکار حافظه و غیره) نیست، طبیعی هست که سرعتش میتونه بالاتر باشه، همونطور که مثلا سرعت سی هم از سی++ بیشتره و این بخاطر اینه که سی سطح پایین تر از سی++ است و شیء گرایی و یکسری امکانات و راحتی ها و امنیت برنامه نویسی سی++ رو نداره. همونطور که سرعت اسمبلی هم از هردوی سی و سی++ بیشتره! کلا هرچی سطح برنامه نویسی و امکانات و راحتی و امنیت اون بالاتر میره، پرفورمنس پایین میاد، چون خیلی کارها باید بصورت خودکار حالا بخشی در در زمان کامپایل و بخشی در زمان اجرا صورت بگیرن برای تامین این راحتی و امنیت و سطح بالا و نوشته شدن کدهای راحتتر و کمتر توسط برنامه نویسان و کاهش خطاهای انسانی و حفره ها و باگهای متداول برنامه نویسی.
پس هروقت شما یه زبان درست کردید که برنامه نویسی درش به اندازهء سی شارپ و دات نت سطح بالا و راحت و سریع و ایمن و با حداقل کد بود، و سرعت برنامه هاش هم از دات نت بالاتر بود و یکسری مزایای جانبی دیگر دات نت رو هم داشت، اونوقت بیاید و بگید که این حرفا غلطه!