SIMD اونقدرها که فکر میکنی سخت نیست
خلاصهٔ کاملتر
نویسنده تو این پست میگه خیلی از مهندسهای خوبی که دیده، SIMD رو یا زیادی پیچیده میدونن یا یه بهینهسازی خاص نرمافزارهای فوقپرسرعت. به نظر او این برداشت غلطه: هر جا حلقهای شبیه for (byte in bytes) روی صدها هزار یا میلیونها مقدار داری، فرصت SIMD وجود داره. شرط سوددهی فقط اینه که حجم داده بهقدر کافی بزرگ باشه؛ برای چند ده بایت ارزشش رو نداره. مثالهای پست با Zig نوشته شدن ولی ایدهاش زبانمستقله.
قالبی که معرفی میکنه پنج مرحلهست: ثابتها رو تو همهٔ لِینها پخش کن (broadcast)، ورودی رو چانکبهچانک به اندازهٔ عرض وکتور بخون، عملیات مقایسه یا حساب رو موازی روی همهٔ لِینها انجام بده، نتیجهٔ وکتوری رو به چیزی که الگوریتم میخواد کاهش بده، و باقیماندهٔ ورودی رو با همون حلقهٔ اسکالر قبلی تموم کن. به گفتهٔ نویسنده مرحلهٔ چهارم بین الگوریتمها بیشترین تفاوت رو داره و بقیه تقریباً همیشه یک شکلان.
مثال واقعی از ترمینال Ghostty ـه: پیدا کردن انتهای یه رشتهٔ کاراکتر قابل چاپ، یعنی حرکت جلو تا رسیدن به مقداری کوچکتر یا مساوی 0xF. نسخهٔ اسکالرش یه خط بود؛ نسخهٔ وکتوری حدود ۱۲ خط بیشتر داره:
const V = @Vector(lanes, u32);
const threshold: V = @splat(0xF);
while (end + lanes <= cps.len) : (end += lanes) {
const values: V = cps[end..][0..lanes].*;
const greater_than_threshold = values > threshold;
if (@reduce(.And, greater_than_threshold)) continue;
}اینجا @splat مقدار 0xF رو تو همهٔ لِینها کپی میکنه، > یه دستور واقعی وکتوری CPU میشه که مثلاً هشت مقایسه رو با یه دستور انجام میده، و @reduce(.And, ...) میگه آیا همهٔ لِینها از آستانه بالاترن یا نه. اگه یکی پایینتر بود، نتیجه با @bitCast به یه عدد صحیح بیتبهلِین تبدیل و با @ctz روی معکوسش، ایندکس اولین لِین ناموفق پیدا میشه. تعداد لِینها روی ARM چهارتا، روی AVX2 هشتتا و روی AVX-512 شونزدهتاست؛ اگه CPU پشتیبانی نکنه، تابع کمکی null برمیگردونه و کل بخش SIMD رد میشه.
نویسنده میگه این تغییر تا ۴ برابر روی ARM NEON، ۸ برابر روی AVX2 و ۱۶ برابر روی AVX-512 توان عملیاتی حلقه رو بالا میبره، و در عمل روی یه دسکتاپ اینتل با AVX2 حدود ۵ برابر بهبود سرتاسری دیده.
دربارهٔ اینکه چرا کامپایلر خودش این کار رو نمیکنه، میگه گاهی میکنه و باید اول نسخهٔ اسکالر رو با بهینهسازی کامپایل کنی و خروجی رو ببینی؛ ولی auto-vectorization در عمل خیلی محدوده و مهمتر اینکه وقتی ۵ برابر سرعت برات مهمه، بهتره وکتوریشدن صریح و قابل پیشبینی باشه تا یه تغییر کد نامرتبط بیسروصدا برش نگردونه به اسکالر.
نکات کلیدی:
- قالب پنجمرحلهای: پخش ثابتها، حلقهٔ چانکی، عملیات موازی، کاهش، دنبالهٔ اسکالر
- دنبالهٔ اسکالر همون حلقهٔ اصلیه و هم باقیمانده رو میگیره هم CPUهای بدون پشتیبانی
- تعداد لِینها به معماری بستگی داره: ۴ در NEON، ۸ در AVX2، ۱۶ در AVX-512
- سود واقعی معمولاً از سود نظری کمتره، ولی هنوز چند برابره
- قبل از نوشتن دستی، خروجی بهینهشدهٔ کامپایلر رو ببین؛ ولی به auto-vectorization تکیه نکن




