حماسه JIT در PHP یا کدهای اسپاگتی ما چگونه قرار است اجرا شوند...

همونطور که میدونید تو نسخه ۸ زبان PHP قراره JIT یا Just In Time compiler اضافه بشه (سند RFC) و از زمانی که این موضوع مطرح شده یه عده ای هم تصور کردن که دیگه میشه باهاش موشک فرستاد فضا.

در این مطلب صحت و سقم این موضوع و اینکه JIT قراره چه کاری کنه رو بررسی می‌کنیم.


در قدم اول باید بفهمیم PHP چطور کدهای اسپاگتی ما را تبدیل به کد ماشین و اجرا میکند.

برای توضیح هر قدم ماجرا روی قطعه کد زیر و مراحل تبدیل آن کار میکنیم:

<?php
$a = 1;
echo $a;

۱- فرآید Lexing یا Tokenizing: در این مرحله کدهای PHP خونده میشه و تبدیل به یک سری کلمه کلیدی (Keyword) می‌شوند، اصطلاحا Token از کد ساخته میشود. این پروسه این امکان را به مفسر میدهد تا بفهمد هر بخش از کد مربوط به کدام قسمت از برنامه است.

کد بالا بعد از توکنایز شدن تبدیل به چیزی شبیه متن زیر میشود:

Line 1: T_OPEN_TAG ('<?php
')
Line 2: T_VARIABLE ('$a')
Line 2: T_WHITESPACE (' ')
string(1) "="
Line 2: T_WHITESPACE (' ')
Line 2: T_LNUMBER ('1')
string(1) ""
Line 2: T_WHITESPACE ('
')
Line 3: T_ECHO ('echo')
Line 3: T_WHITESPACE (' ')
Line 3: T_VARIABLE ('$a')
string(1) ""

همانطور که می‌بینید مفسر قدم به قدم هر بخش کد را که لازم بوده تبدیل به توکن کرده و کد برای ماشین یک مرحله قابل فهم تر شده.


۲- فرآیند Parsing: حالا که مفسر Token ها را در اختیار دارد شروع به آنالیز توکن ها میکند تا از این طریق عملیات ها را درک کند و البته مفسر در این مرحله توکن‌ها را با دیکشنری کلمات کلیدی زبان هم تطبیق میدهد تا مطمئن شود دستورات صحیحی ارسال شده و در نهایت AST یا Abstract Syntax Tree ساخته میشه. (فرآیند Parsing جزییات بیشتری دارد، از آنجا که صرفا فرآیندها به صورت خلاصه بررسی می‌شوند از ذکر جزییات اضافه خودداری می‌کنم)

برای کد بالا حالا چنین خروجی خواهیم داشت:

AST_STMT_LIST
    0: AST_ASSIGN
        var: AST_VAR
            name: "a"
        expr: 1
    1: AST_ECHO
        expr: AST_VAR
            name: "a"

به صورت خلاصه خروجی بالا به معنای زیر است:

متغیر a را بساز و مقدار دهی کن، سپس متغیر a را چاپ کن.

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

۳- فرآیند Compile: با داشتن AST پروسه ها و تقدم و تاخرهای کد راحتتر قابل تشخیص است. برای تبدیل AST به عملیات قابل اجرا توسط ماشین نیازمند IR یا Intermediate Representation هستیم. در این مرحله کدی نزدیک به کد ماشین داریم که میتواند توسط Zend Engine اجرا شود.

برای نمونه کد بالا، خروجی کامپایل شده زیر را خواهیم داشت:

این خروجی توسط اکستنشن VLD آماده شده و البته در این خروجی هیچ بهینه سازی صورت نگرفته.

دستوری که برای این خروجی استفاده شده را در زیر می‌توانید ببینید:

php -dopcache.enable_cli=1 -dopcache.optimization_level=0 -dvld.active=1 -dvld.execute=0 file.php

نمونه‌ی بالا یک Opcode است.

۴- با داشتن Opcode میتوانیم دستورات را برای اجرا تحویل CPU دهیم. این کار از طریق ماشین مجازی Zend Engine انجام میشود که قادر است Opcode را بخواند و اجرا کند و در نهایت با اتمام اجرای تمام Opcode ها ماشین مجازی کارش به اتمام میرسد.

در نتیجه شمای کلی اجرای یک کد PHP به شکل زیر است:

فرآیند اجرای یک کد PHP
فرآیند اجرای یک کد PHP



این فرآیند طولانی و زمانبر است و تا قبل از انتشار اکستنشن Opcache دقیقا تمام کدهای PHP به همین شکل با هر بار درخواستی از سمت وب سرور یا CLI اجرا می‌شدند. یعنی هر بار فرآیند Token->AST->Opcode به ازای هر درخواست اجرا می‌شد.

حتما به این موضوع دارید فکر میکنید که خب کد پروداکشن معمولا با سرعت زیادی تغییر نمیکند و عموما ثابت است و اجرای این فرآیند هم منابع زیادی از سیستم مصرف میکنه پس چطور می‌شود این فرآیند را تسریع کرد. اینجا پای Opcache به ماجرا باز می‌شود.

اگر به خاطر داشته باشید در مرحله سوم و درست قبل از رسیدن به Zend Engine کدهای Opcode را داشتیم. کاری که Opcache انجام میدهد، استفاده از یک مموری اشتراکی برای نگه‌داری قطعه کدهای Opcode است. بنابراین در یک SAPI نظیر php-fpm و یا CLI این امکان فراهم میشود که اول چک کنیم Opcode مرتبط با یک فایل وجود دارد یا نه و در صورتی که وجود داشته باشد مستقیما Opcode را برای پردازش به Zend Engine بفرستیم.

به این موضوع توجه کنید که Zend Engine عملا آخرین مرحله تبدیل Opcode به CPU Instruction را انجام می دهد.

خب پس با حضور Opcache شمای بالای ما به شکل زیر تغییر میکنn:

فرآیند اجرای کد PHP وقتی از Opcache استفاده میکنیم
فرآیند اجرای کد PHP وقتی از Opcache استفاده میکنیم



خب در نهایت می‌رسیم به اصل ماجرا که همانا JIT است. بخش زیادی از باری که بابت هر بار پردازش به سیستم تحمیل میشود با استفاده از Opcache کاهش می‌یابد و در کنار بسیار از بهینه سازی‌هایی که در نسخه ۷ زبان صورت گرفت، بهینه سازی این اکستنشن هم عامل مهم دیگری در افزایش سرعت اجرای نرم‌افزارها بود.

در اینجا بد نیست اشاره کنم قابلیت Preloading در PHP 7.4 هم به شما این امکان را می‌دهد که بخشی از کدها را حین اجرای سرویس PHP کش کنید. برای جزییات بیشتر به RFC مراجعه کنید.


حالا تصور کنید که شرایطی وجود داشت که Opcode ها لازم نبود به Zend Engine فرستاده شوند این امکان وجود داشت که خروجی Zend Engine جایی نگه‌داری و مجددا استفاده شود. این دقیقا کاریست که JIT در نسخه آتی PHP قرار است انجام دهد. بخشی از Opcode ها توسط JIT تبدیل به کد قابل فهم برای CPU شده و کش می‌شوند و در اجرای مجدد به جای ارسال Opcode به Zend Engine عملا دستورات مستقیما به CPU برای انجام عملیات‌ها فرستاده می‌شود.

پیش بینی می‌شود با حضور JIT بهره‌وری نرم‌افزارهای نوشته شده با PHP تا حدی افزایش پیدا کند که البته این مساله بسیار به خود کد وابسته است. بنابراین انتظار بهبود چشمگیر لزوما نباید داشته باشیم.

بد نیست این نکته را هم اضافه کنم که JIT به صورت یک ویژگی در اکستنش Opcache قرار است پیاده سازی شود.

در نهایت شمای کلی اجرای کد PHP در نسخه ۸ به شکل زیر خواهد بود.


فرآیند اجرای کد در PHP 8
فرآیند اجرای کد در PHP 8



تلاش کردم مطلب بالا عاری از مشکل باشه و تا حد ممکنه با منابع مختلف سعی کردم صحت این اطلاعات و محتوا بررسی بشه. با این حال اگر اشکال فنی در این مطلب وجود داره ممنون میشم در کامنت‌ها ذکر کنید تا مطلب را اصلاح کنم.


یکی از منابعی که بسیار در نوشتن این مطلب کمکم کرد از بلاگ thephp.website بود.