ماکروهای likely و unlikely: ترفندی از کرنل لینوکس برای سرعت بیشتر
خلاصهٔ کاملتر
تو کدهای کرنل لینوکس (و خیلی جای دیگه) گاهی میبینی یه شرط if داخل ماکروی unlikely پیچیده شده. نویسنده تو این پست میگه این یه ترفند سطحپایینه که اجازه میده به کامپایلر بگی کدوم شاخهی if قراره کمتر اتفاق بیفته، و همین یه خط میتونه رو سرعت برنامه اثر جدی بذاره.
unlikely فقط یه ماکروی سادهست:
#define unlikely(e) __builtin_expect(!!(e), 0)این ماکرو از تابع ویژهی __builtin_expect توی GCC استفاده میکنه تا یه سرنخ اولیه به کامپایلر بده که مقدار شرط احتمالاً چیه. کامپایلر با این اطلاعات، پردازنده رو از قبل برای branch prediction (یعنی حدس زدن اینکه بعد از یه شرط کدوم مسیر اجرا میشه) آماده میکنه.
برای نشون دادن اثرش، نویسنده دو تا حلقهی کاملاً یکسان نوشته؛ یکی با unlikely و یکی با likely روی همون شرط، و بعد احتمال true شدن شرط رو از ۵۰٪ تا ۰.۱٪ کم کرده. وقتی احتمال واقعیِ رسیدن به if پایین بود (همون چیزی که unlikely بهش اشاره میکنه)، نسخهی unlikely تقریباً ثابت موند؛ ولی نسخهای که اشتباهی با likely علامت خورده بود، هرچی احتمال کمتر میشد کندتر اجرا میشد. مثلاً روی احتمال ۱۰٪، نسخهی unlikely حدود ۵۱ هزار میکروثانیه طول کشید ولی همون کد با هینت غلط likely به حدود ۷۶ هزار میکروثانیه رسید.
دلیلش رو نویسنده تو اسمبلی خروجی gcc -O2 نشون داده: کامپایلر بر اساس هینت، ترتیب دستورها رو عوض میکنه. تو نسخهی likely کد شاخهی «محتمل» درست بعد از چک شرط قرار میگیره، ولی تو unlikely برعکسه. این ترتیب مهمه چون پردازنده معمولاً دستورها رو pipeline میکنه، یعنی قبل از تموم شدن دستور فعلی، دستورهای بعدی رو از حافظه پیشواکشی و اجرا میکنه؛ اگه شاخهی نادرست جلوتر باشه، این پیشواکشی هدر میره و کارایی افت میکنه.
نکات کلیدی:
- unlikely(e) معادل __builtin_expect(!!(e), 0) هست؛ فقط یه سرنخ برای کامپایلره، نه یه شرط واقعی.
- تو تست نویسنده با gcc -O2، وقتی هینت اشتباه بود (شرط واقعاً بهندرت رخ میداد ولی likely علامت خورده بود)، زمان اجرا نسبت به هینت درست حدود ۳۵ تا ۴۷ درصد بیشتر شد.
- دلیل این افت، تغییر ترتیب دستورهای اسمبلی توسط کامپایلره که باعث میشه پیشواکشیِ پردازنده کارآمد نباشه.
- الگوی if (unlikely(...)) تو کدهای کرنل لینوکس خیلی زیاد استفاده شده.