اگر امروز برای بهینهسازی کدهای محاسباتی خود در Go به سراغ اسمبلی میروید، احتمالاً میدانید که این مسیر چقدر دشوار و وابسته به سختافزار است. توسعهدهندگان سالها مجبور بودند یا عملکرد کندتر محاسبات اسکالر (تکدادهای) را بپذیرند یا به پیچیدگیهای اسمبلی مخصوص هر معماری شیرجه بزنند. اما حالا با معرفی قابلیتهای جدید در نسخه 1.27، این بنبستِ قدیمی شکسته شده است.
طبق اعلام تیم توسعه Go در ۲۵ سپتامبر ۲۰۲۶، رابطهای برنامهنویسی (API) تجربی SIMD (Single Instruction Multiple Data) به زبان اضافه شدهاند. SIMD — شبیه به یک آشپز که بهجای خرد کردن تکتک هویجها، با یک ضربهٔ چاقوی پهن، هشت هویج را همزمان میبرد — به پردازنده اجازه میدهد یک عملیات واحد را روی مجموعهای از دادهها بهطور همزمان اجرا کند. برای مثال، یک CPU میتواند بهجای هشت چرخه، هشت جفت عدد اعشاری ۶۴ بیتی را در یک چرخه جمع بزند. این توانایی برای رمزنگاری، هوش مصنوعی و پردازش دادههای حجیم حیاتی است؛ حتی سیستم مدیریت حافظه (Garbage Collector) در Go نیز برای سرعت بخشیدن به اسکن حافظه از این تکنیک استفاده میکند. این رویکرد بهینهسازی در راستای تلاشهای گستردهتر برای ارتقای توان محاسباتی Go است، مشابه آنچه اخیراً در ادغام حلکنندههای سریع MIP در سرویسهای ابری Go شاهد بودیم تا پیچیدهترین مسائل ریاضی با سرعت بیشتری حل شوند.
تا پیش از این، دسترسی به این قدرت پنهان CPU تنها برای کسانی ممکن بود که اسمبلی مینوشتند. در حالی که دستاوردهای عملکردی بسیار عظیم بود، اما تلاش برای نگهداری فایلهای اسمبلی مجزا برای x86 و ARM به این معنا بود که بخش بزرگی از توان سختافزاری در برنامههای استاندارد Go بلااستفاده میماند.
شکاف معماری و چالشهای پورتشدن
به گزارش مستندات Go، SIMD بهطور بدنامی در سختافزارهای مختلف پراکنده و تکهتکه است. تیم Go سه بعد اصلی از تغییرات را شناسایی کرده است که پورت کردن SIMD را دشوار میکند:
- اندازه بردار (Vector Size): برخی پلتفرمها از بردارهای ثابت ۱۲۸ بیتی استفاده میکنند (مانند wasm، PowerPC و s390x). برخی دیگر اندازههای ثابت متعددی را ارائه میدهند، مانند amd64 (۱۲۸، ۲۵۶ و ۵۱۲ بیت) و loong64 (۱۲۸ و ۲۵۶ بیت). معماری Riscv64 از اندازههای نامشخص بین ۱۲۸ تا ۶۵,۵۳۶ بیت پشتیبانی میکند، هرچند طول بردار محدود به توانهای ۲ است. Arm64 نیز یک اندازه ثابت (128-bit NEON) و یک اندازه متغیر (SVE بین ۱۲۸ تا ۲۰۴۸ بیت، فقط توانهای ۲) را پشتیبانی میکند.
- منطق ماسکگذاری (Masking Logic): نحوه مدیریت منطق «اگر-آنگاه-وگرنه» در بردارهای مختلف بهشدت متفاوت است. برخی از بیتماسکها و عملیات بولی استفاده میکنند (مانند AVX، AVX2، NEON و wasm). برخی دیگر از ثباتهای ماسک اختصاصی استفاده میکنند که در آن هر بیت بر یک المان بردار حاکم است (مانند AVX512 و RVV). در SVE، برای هر بایت بردار یک بیت اختصاص مییابد و از کمارزشترین بیت برای عملیات ماسکشده استفاده میشود. AVX2 همچنین از بارگذاری و ذخیرهسازی ماسکشده با استفاده از یک بردار ساده به عنوان ماسک پشتیبانی میکند که در آن پرارزشترین بیت، حاکم بر عملیات است.
- مجموعه دستورالعملها (Instruction Sets): پشتیبانی از محاسبات پایه متفاوت است؛ برای مثال، wasm فاقد مقایسههای مربوط به بردارهای صحیح ۶۴ بیتی است. ابزارهای اولیه برای بازآرایی (Rearranging) المانهای بردار نیز متفاوت است؛ برخی به ورودیهای ثابت نیاز دارند و برخی از ورودیهای متغیر پشتیبانی میکنند. همچنین عملیاتهای مربوط به رمزنگاری در هر معماری متفاوت است.
برای حل این مشکل، ابتدا بسته archsimd در نسخه 1.26 برای amd64 معرفی شد و سپس در نسخه 1.27 پشتیبانی از arm64 (بهطور خاص NEON) و wasm اضافه گردید. اما archsimd همچنان وابسته به معماری است، به این معنی که توسعهدهنده همچنان باید تفاوتهای بین پلتفرمها را مدیریت کند. با وجود اینکه archsimd برای یکپارچگی طراحی شده بود، اما بسیاری از پیچیدگیها باقی ماندند و تستهای چندپلتفرمی را دشوار کردند.
بسته جدید و متقاطع simd
در نسخه 1.27، بسته سطحبالای simd معرفی شده است که با الهام از کتابخانه Highway در C++ طراحی شده و کاملاً مستقل از اندازه بردار و نوع سختافزار است. این رابط، تفاوتهای سختافزاری را با حذف کامل بردارهای با اندازه ثابت از سیستم تایپ (Type System) پنهان میکند.
این بسته تنها عملیاتی را پشتیبانی میکند که در اشتراک (Intersection) تمام پلتفرمهای مورد حمایت باشند. اگر یک پلتفرم خاص فاقد یک دستورالعمل مورد نیاز باشد، بسته simd یک شبیهسازی (Emulation) بهینه با استفاده از سایر دستورات SIMD موجود ارائه میدهد. اگر سختافزار اصلاً هیچ پشتیبانی از SIMD نداشته باشد، بسته به شبیهسازی اسکالر استاندارد بازمیگردد تا کد همیشه اجرا شود.
توسعهدهندگان میتوانند این قابلیت را با پرچم GOEXPERIMENT=simd فعال کنند. در این API، انواع دادهها بهصورت جمعی و با حروف بزرگ تعریف شدهاند (مانند simd.Uint8s یا simd.Float32s). بردارها با استفاده از توابعی مانند simd.LoadFloat32s از اسلایسها بارگذاری شده و با simd.Store ذخیره میشوند.
قابلیتهای فنی API
بسته simd مجموعهای جامع از عملیاتها را برای انواع مختلف (از Int8s تا Float64s) ارائه میدهد که به شرح زیر دستهبندی میشوند:
- بارگذاری و پخش (Load/Broadcast): شامل
LoadVوLoadVPartبرای اسلایسهای جزئی، وBroadcastVبرای پر کردن یک بردار با یک مقدار اسکالر است. تمام اینها برای انواع داده از Int8s تا Float64s پشتیبانی میشوند. - محاسبات (Arithmetic): پشتیبانی از
Add،Sub،Mul،Abs،NegوSqrt. عملیاتهای تخصصی مانندAddSaturatedوSubSaturatedبرای انواع اعداد صحیح در دسترس هستند.MulAddبرای انواع اعشاری پشتیبانی میشود. متدهایIfElseوMaskedاجازه انجام عملیات شرطی بر اساس یک ماسک را میدهند. - منطق و ماسکگذاری (Boolean & Masking): شامل
And،Or،Xor،NotوAndNot. متدIfElseامکان انتخاب شرطی بر اساس ماسک را فراهم میکند. همچنینCarrylessMultiplyEvenوCarrylessMultiplyOddبهترتیب برای Uint8s و Uint16s پشتیبانی میشوند. - مقایسهها (Comparisons): عملیاتی مانند
Equal،Greater،LessوNotEqualمقادیر ماسک تولید میکنند (مثلاًMask8sبرایInt8s) که برای فیلتر کردن استفاده میشوند.GreaterEqualوLessEqualنیز ارائه شدهاند. - تبدیل و تغییر شکل (Conversions & Reshaping): پشتیبانی از
ConvertToFloatW،ConvertToIntWوConvertToUintW. تغییر شکل با هزینه صفر (Zero-cost reshaping) اجازه جابهجایی بین انواع را میدهد، مانندReshapeToUint8s،ReshapeToUint16s،ReshapeToUint32sیاReshapeToUint64s. همچنینToBits،BitsToFloatWوBitsToIntWامکان کستینگ در سطح بیت را فراهم میکنند. - شیفت و چرخش (Shift/Rotate): ارائه
ShiftAllLeft،ShiftAllRight،RotateAllLeftوRotateAllRightبرای بردارهای اعداد صحیح بدون علامت (از Uint8s تا Uint64s). - متدهای ماسک (Mask Methods): انواع ماسک (مانند Mask8s و Mask16s) از
And،Or،Stringو تبدیلهایToIntWsپشتیبانی میکنند.
پیادهسازی فنی و توزیع دستورات
برای رسیدن به کارایی نزدیک به اسمبلی، کامپایلر Go از یک استراتژی پیچیده بازنویسی AST استفاده میکند. کامپایلر نسخههای تخصصی از توابع و متغیرها را ایجاد کرده و انواع عمومی simd را با انواع تخصصی بر اساس اندازه در یک بسته داخلی پل (simd/internal/bridge) جایگزین میکند.
این نسخههای تخصصی با پسوندهایی مانند @simdNNN (مثلاً ۱۲۸، ۲۵۶، ۵۱۲ یا ۰ برای شبیهسازی) مشخص میشوند. کامپایلر هزینه توزیع (Dispatch overhead) را تا حد امکان به سطوح بالاتر منتقل میکند. این بدان معناست که برنامه در لحظه شروع تصمیم میگیرد از چه طول برداری استفاده کند و فراخوانیهای بعدی در حلقه محاسباتی بدون تکرار سربار توزیع انجام میشوند. توابع تخصصی مستقیماً یکدیگر را فراخوانی میکنند که امکان Inlining را فراهم میکند.
اگر توزیع در سطح پایین محاسبات قرار داشته باشد، توسعهدهندگان میتوانند با اضافه کردن یک اشاره غیرضروری به نوع simd در تابع فراخوان، آن را به سمت بالا سوق دهند. این کار تضمین میکند که بنچمارک یا حلقه، مستقیماً نسخه تخصصی (مثلاً vpsumd3) را فراخوانی کند.
گذار به کدهای وابسته به سختافزار
برای موارد خاصی که API متقاطع بیش از حد محدود است، بسته simd اجازه گذار به کدهای وابسته به معماری را میدهد. توسعهدهندگان میتوانند از متد ToArch() برای تبدیل یک بردار متقاطع به یک نوع archsimd استفاده کنند.
این امر یک رویکرد «ترکیبی» را ممکن میکند: ۹۰٪ الگوریتم را با کد متقاطع simd بنویسید و تنها ۱۰٪ باقیمانده را در بلوکهای وابسته به معماری پیاده کنید. برای مثال، اگر توسعهدهنده به OnesCount نیاز داشته باشد (که در Go 1.27 غایب است)، میتواند یک Type Switch روی v.ToArch() پیاده کند تا در AVX512 از archsimd.Int8x64.OnesCount() استفاده کند و برای سایر پلتفرمها یک شبیهسازی دستی ارائه دهد.
در یک پیادهسازی واقعی برای OnesCount روی amd64، کد بین archsimd.Int8x16 (با استفاده از جدول جستجو برای AVX/AVX2)، archsimd.Int8x32 و archsimd.Int8x64 (با استفاده از دستور بومی برای AVX512) سوئیچ میکند. برای arm64 و wasm، پیادهسازی سادهتر است زیرا آنها از OnesCount بهطور بومی از طریق انواع archsimd پشتیبانی میکنند. کامپایلر سربار Type-switch را در این موارد بهینه میکند تا عملکرد افت نکند. برای بازگشت به API متقاطع، توسعهدهندگان از توابع simd.<SimdType>FromArch استفاده میکنند.
استراتژیهای شبیهسازی
به دلیل اینکه بسته simd اشتراکات تمام پلتفرمها را هدف قرار داده است، بسیاری از حفرهها با شبیهسازی پر شدهاند. برخی از اینها ساده هستند، مانند ارائه هر دو متد Int8x16.Add و Uint8x16.Add در حالی که هر دو از یک دستور سختافزاری یکسان استفاده میکنند. برخی دیگر پیچیدهترند:
- شیفت اسکالر: در معماریهایی که فقط از فواصل شیفت یکنواخت پشتیبانی میکنند، با استفاده از شیفتهای برداری شبیهسازی میشود.
- مقایسههای بدون علامت: به صورت مقایسههای علامتدار ترکیب شده با دو عملیات XOR با یک مقدار ثابت پیاده شدهاند.
- ضرب بدون انتقال (Carryless Multiply): به صورت یک شبیهسازی با زمان ثابت (Constant-time) ارائه شده است تا امنیت رمزنگاری در پلتفرمهای فاقد دستور سختافزاری تضمین شود.
- کاهش مجموع (Sum Reduction): در حالی که
ReduceSumبرای نسخه بعدی برنامهریزی شده است، نسخههای فعلی ممکن است نیاز داشته باشند بردار را در یک اسلایس ذخیره کرده و مقادیر اسکالر را بهصورت دستی جمع کنند. دلیل این امر نبود یک روش مشترک برای جمع تمام المانهای یک بردار در تمامی پلتفرمها است.
تست و دیباگ با GODEBUG
از آنجایی که رفتار SIMD به سختافزار وابسته است، تیم تنظیمات GODEBUG را برای شبیهسازی محیطهای مختلف اضافه کرده است:
GODEBUG=simd=0: فارغ از سختافزار، شبیهسازی کامل را اجبار میکند.GODEBUG=simd=128/256/512: طولهای برداری خاصی را اجبار میکند. اگر این ویژگیها در دسترس نباشند، برنامه بلافاصله Panic میکند.GODEBUG=simd=+128/256/512: از طول مشخص شده استفاده میکند حتی اگر برخی ویژگیها مفقود باشند. اگر به یک دستور پشتیبانینشده برسد، Panic میکند؛ در غیر این صورت اجرا میشود. این برای شبیهسازی رزبریپای (NEON بدون PMULL) یا شبیهساز amd64 در اپل سیلیکون (AVX2 بدون VPCLMULQDQ) مفید است.
نقشه راه آینده
بر اساس گزارش go.dev، تیم توسعه قصد دارد در نسخه 1.28 پشتیبانی از SVE (Scalable Vector Extension) را برای arm64 اضافه کند. آنها همچنین قصد دارند API متقاطع را گسترش دهند تا شامل ReduceSum (جمع افقی)، OnesCount (شمارش بیتهای یک) و عملیاتهای پیشرفتهتر جابهجایی بردار (Shuffling) و ماسک شود.
نسخه 1.28 همچنین شامل «گونههای ویژگی» (Feature Variants) خواهد بود تا پلتفرمهایی که اکثر پشتیبانیهای سختافزاری را دارند (مانند رزبریپای)، مجبور نباشند بهخاطر یک عملیات مفقود، تمام کد را به شبیهسازی اسکالر کامل تنزل دهند.
این تغییر، فرض بنیادین درباره کارایی Go را عوض میکند. پردازش دادههای با توان عملیاتی بالا دیگر نیازمند یک برنامهنویس متخصص اسمبلی در تیم نیست. Go با ارائه رابط «یکبار بنویس، سریع اجرا کن»، خود را به زبانی ایدهآل برای زیرساختهای مدرن هوش مصنوعی و رمزنگاری تبدیل میکند.
برای شروع آزمایش، توسعهدهندگان باید به Go 1.27 بهروزرسانی کرده و پرچم GOEXPERIMENT=simd را در محیط خود فعال کنند.
گام بعدی شما
- اگر پروژهای با محاسبات سنگین روی آرایهها دارید، Go 1.27 را نصب کرده و پرچم
GOEXPERIMENT=simdرا فعال کنید. - بخشهای حساس به کارایی (Hot-paths) کد خود را با انواع
simd.Float32sیاsimd.Uint8sبازنویسی کنید. - از
GODEBUGبرای اطمینان از صحت عملکرد کد روی معماریهای مختلف استفاده کنید.
اما داستان سختافزاری این تحول حتی شگفتانگیزتر است — به تحلیل ما دربارهی تراشههای Blackwell و اثر آنها بر زبانهای سیستمی مراجعه کنید.




گفتگو