توابع سره در ‪C++‬

یکی از امکانات بهینه‌سازی که بین برنامه‌نویس‌های سی کمتر شناخته شده؛ امکان تعریف توابع به‌صورت سره هست. این امکان دامنهٔ گسترده‌ای از بهینه‌سازی‌ها رو امکان‌پذیر می‌کنه و حتماً باید جایی که کارایی مهم هست از این ویژگی استفاده کنید. در این پست به معرفی و بررسی اثر توابع سره می‌پردازم.

اهمیت اثر جانبی

زبان برنامه‌نویسی ‪C++‬ دقیقا مثل پدربزرگش C ؛ یک زبان دستوری است. بنابراین توابع این زبان میتونن اثر جانبی داشته باشند. اثر جانبی یک تابع یعنی اثری که اون تابع در حالت سیستم ایجاد می‌کنه؛ البته مقدار برگشتی از تابع جزو اثر جانبی حساب نمیشه. زبان‌های برنامه‌نویسی تابعی، اثر جانبی رو محدود می‌کنند. یعنی هر تابع یا اثر جانبی نداره یا فقط محدود به IO هست. برای مثال تغییر یک متغیر global یک اثر جانبی از اجرای یک تابع حساب میشه. به تابعی که اثر جانبی نداشته باشه تابع سره یا تابع محض گفته میشه. توابع محض ویژگی‌های بسیار جالبی دارند که یکی از این ویژگی‌ها یعنی «شفافیت ارجاع» کمک بسیار بزرگی به بهینه‌سازی کدها میکنه. به‌طور خلاصه اصل شفافیت مرجع میگه که اگر یک تابع، سره باشه، همیشه میشه نتیجهٔ اجرای اون تابع با آرگومان‌های یکسان رو با کد فراخوانیِ تابع جایگزین کرد.

برای مثال تابع pow2 رو درنظر بگیرید. این تابع بسیار ساده است، صرفاً یک عدد میگیره و توان دوم اون رو برمیگردونه:

double pow2(double x) { 
    retutn x*x; 
}
به‌طور مشخص این تابع یک تابع سره است. یعنی شما هر جای برنامه می‌تونید ‪pow2(5)‬ رو با نتیجه، یعنی 25، جایگزین کنید. این ویژگی برای بهینه‌سازی بسیار مفید هست. به این صورت که کامپایلر، زمان کامپایل برنامه می‌تونه فراخوانی‌های تکراری رو حذف کنه.

از آنجایی که ‪C++‬ یک زبان برنامه‌نویسی تابعی نیست؛ توابع سره نداره. در مورد تمام توابع، کامپایلر فرض می‌کنه اثر جانبی وجود داره. و هیچ بهینه‌سازی‌ای برمبنای شفافیت ارجاع انجام نمیده.

بررسی شفافیت ارجاع در ‪C++‬

برای این که ببینیم شفافیت ارجاع در ‪C++‬ وجود نداره، نتیجهٔ خروجی کامپایل کد برای همین تابع سادهٔ توان دو رو بررسی می‌کنیم. خروجی اسمبلی کد پایین رو ملاحظه کنید:

double pow2(double x) { return x*x; }
// ... 
double caller() {
    const double x = pow2(5.0) + pow2(5.0);
    const double y = pow2(5.0) + pow2(5.0);
    return x+y;
}

این کد با gcc نسخهٔ ۱۰ و با آخرین درجهٔ بهینه‌سازی (‪-O3‬) کامپایل شده. قبل از این که خودتون بخواهید این رو تست کنید باید اشاره کنم که حتما باید پیاده‌سازی تابع pow2 خارج از برنامه باشه. مثلاً توی یک کتابخونه یا آبجکت متفاوت. در غیر این صورت کامپایلر طوری بهینه‌سازی می‌کنه که مثال سادهٔ ما بی‌ارزش میشه (: بنابراین ما فرض می‌کنیم در لحظهٔ کامپایل؛ کامپایلر ما اطلاعی از پیاده‌سازی تابع pow2 نداره. خروجی اسمبلی تابع caller رو مشاهده کنید:

caller():
  sub     rsp, 24
  movsd   xmm0, QWORD PTR .LC0[rip]
  call    pow2(double)
  movsd   QWORD PTR [rsp], xmm0
  movsd   xmm0, QWORD PTR .LC0[rip]
  call    pow2(double)
  movsd   xmm1, QWORD PTR [rsp]
  addsd   xmm1, xmm0
  movsd   xmm0, QWORD PTR .LC0[rip]
  movsd   QWORD PTR [rsp], xmm1
  call    pow2(double)
  movsd   QWORD PTR [rsp+8], xmm0
  movsd   xmm0, QWORD PTR .LC0[rip]
  call    pow2(double)
  addsd   xmm0, QWORD PTR [rsp+8]
  addsd   xmm0, QWORD PTR [rsp]
  add     rsp, 24
  ret
.LC0:
  .long   0
  .long   1075052544

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

اعلان توابع سره

در ‪g++‬ و clang میشه تابع رو به روش زیر به‌صورت محض اعلان کرد:

[[gnu::pure]] double pow2(double x);

اضافه کردن ویژگی pure به تابع، به کامپایلر اعلام می‌کنه که تابع قرار نیست وضعیت برنامه رو عوض کنه و تابع سره یا محض است. بنابراین با این کار به کامپایلر اجازه میدیم از بهینه‌سازی‌های مبنی‌بر شفافیت مرجع استفاده کنه. این کار توی برنامه‌نویسی سیستم‌های امبدد و میکروکنترلرها، و همچنین برنامه‌های محاسباتی موازی و سنگین خیلی می‌تونه مفید باشه. کد تغییر یافته و خروجی اسمبلی همین برنامه رو ببینیم:

caller():
  sub     rsp, 8
  movsd   xmm0, QWORD PTR .LC0[rip]
  call    pow2(double)
  add     rsp, 8
  addsd   xmm0, xmm0
  addsd   xmm0, xmm0
  ret
.LC0:
  .long   0
  .long   1075052544

همون‌طور که مشاهده می‌کنید؛ فقط یک فراخوانی از تابع pow2 وجود داره. چون همهٔ آرگومان‌ها 5.0 بودند؛ کامپایلر سه فرخوانی رو حذف کرده. دقت داشته باشید که ترتیب فراخوانی و مکان فراخوانی هم هیچ اهمیتی نداره. یعنی اگر تابع pow2 رو توی تابع دیگه‌ای هم با همین آرگومان فراخوانی کنید؛ کامپایلر باز می‌تونه بهینه‌سازی انجام بده.

هرچند اعلان‌های pure و هم‌خانواده‌های اون هنوز استاندارد نیستند، ولی استفاده از آن‌ها نسبتاً ساده و ایمن هست. کامپایلرهای مدرن معمولاً سینتکس مشابهی رو برای این بهینه‌سازی استفاده می‌کنند. در مواردی که سینتکس متفاوت هست شما می‌تونید اعلان رو براساس پیش‌پردازنده‌های زمان کامپایل؛ به‌صورت اختصاصی کامپایل کنید.

اعلان توابع ثابت

توابع سره تضمین می‌کنند که فراخوانی آن‌ها تغییری در وضعیت برنامه ایجاد نمی‌کنه. یک سطح بالاتری از تضمین هم وجود داره که میگه وضعیت برنامه هم تغییری در مقدار محاسبه شدهٔ تابع ایجاد نمی‌کنه. این نوع توابع ثابت (const) هستند. دقت داشته باشید که این const با cv specifier تفاوت داره. اعلان یک تابع به‌صورت ثابت باعث میشه که خوندن وضعیت برنامه (مثلاً متغیرهای read-only)، به‌نحوی که بتونه توی خروجی تابع تأثیر بذاره نامعتبر بشه. در واقع تضمین قوی‌تری برای محض بودن تابع هست و بهینه‌سازی‌های بیشتری رو ممکن می‌کنه. برای تعریف تابع ثابت از این کد استفاده کنید:

[[gnu::const]] double pow2(double x);

مقایسه با constexpr

ممکنه فکر کنید که توابع const و یا pure همون توابع constexpr هستند. این درست نیست. تفاوت‌های زیادی بین این سه تا وجود داره. مهم‌ترین‌ش این که تابع constexpr زمان کامپایل باید قابل ارزیابی باشه. بنابراین تمام ورودی‌ها باید از نوع لیترال باشند یا همون خود constexpr باشند. در حالی که توابع سره؛ توابع کاملاً معمولی هستند. هر نوع آرگومانی می‌تونید بهشون بدید و مهم نیست تابع و آرگومان‌های ورودی اون زمان کامپایل قابل ارزیابی باشه یا نه. صرفاً این تضمین وجود داره که فراخوانی با آرگومان‌های یکسان، اثر یکسان خواهد داشت.

استاندارد

همونطور که قبلاً اشاره کردم؛ این امکانات استاندارد نیستند. بحث‌هایی وجود داشت که توابع سره وارد استاندارد C++20 بشه اما این اتفاق نیفتاد. دلیلش اینه که بررسی سره بودن تابع بسیار سخت هست. اگر تابع سره نباشه ولی شما اون به شکل سره علامت‌گذاری کنید؛ صرفاً نتایج محاسباتی شما غلط از آب در میاد و کامپایلر راهی برای warning دادن یا خطا گرفتن (به شکل مؤثر) نداره. بنابراین این ویژگی فعلاً به‌صورت غیراستاندارد در دسترس قرار گرفته.

دیدگاه‌ها

comments powered by Disqus