SIMD مستقل از پلتفرم در Go
آدرس مقاله: https://go.dev/blog/simd-experiment آدرس نظرات: https://news.ycombinator.com/item?id=49843269 امتیاز: 238 # نظرات: 86
دیوید چیس و جونیانگ شائو 24 سپتامبر 2026
Go 1.26 و 1.27 شامل APIهای آزمایشی برای عملیات تک دستور العمل چندگانه (SIMD) است. SIMD یک ویژگی بومی بسیاری از CPUهای مدرن است که به نرم افزار اجازه می دهد تا عملیات یکنواخت را در بین بردارهای داده بسیار سریع انجام دهد، مانند افزودن 8 جفت مقدار float64 در یک دستورالعمل. این می تواند به طور قابل توجهی سرعت بسیاری از وظایف محاسباتی را افزایش دهد، از رمزنگاری گرفته تا پردازش داده ها و هوش مصنوعی. در حقیقت، جمعآوری زباله سبز چای Go از SIMD برای تسریع حافظه اسکن اشیاء زنده استفاده میکند.
قبل از این APIهای آزمایشی جدید، تنها راه دسترسی به این قابلیت از Go نوشتن اسمبلی Go بود. این فقط برای هستههای محاسباتی واقعا حیاتی از نظر عملکرد ارزشش را داشت، که به این معنی بود که نرمافزارهای زیادی که میتوانستند از SIMD بهرهمند شوند، بخش زیادی از CPU را بدون استفاده میگذارند.
Go 1.26 یک API SIMD را برای amd64 معرفی کرد و Go 1.27 APIهایی را برای arm64 (مخصوصا NEON) و wasm اضافه کرد. با این حال، یک چالش اساسی برای API SIMD، تنوع بسیار زیاد بین پلتفرمها است، نه صرفا در عملیاتی که آنها پشتیبانی میکنند، بلکه حتی در نحوه نمایش بردارها. برخی از پلتفرمها بردارهایی با اندازه ثابت ارائه میکنند، معمولا بین 128 بیت تا 512 بیت، در حالی که در برخی دیگر اندازه برداری در زمان ساخت مشخص نیست و باید هنگام شروع برنامه مورد بررسی قرار گیرد. برای دسترسی کامل به وسعت این پلتفرمها، این APIها در یک بسته archsimd وابسته به معماری زندگی میکنند.
اما Go 1.27 فراتر از این APIهای وابسته به معماری است و یک رابط SIMD آزمایشی، کاملا قابل حمل، پلتفرم و اندازه آگنوستیک را معرفی میکند، که بر اساس Highway برای C++ است. هدف، پشتیبانی از کد "simd" با عملکرد نزدیک به asm یک بار در پلتفرمهای دارای پشتیبانی SIMD و ارائه یک شبیهسازی مناسب در پلتفرمهایی است که (هنوز) از SIMD پشتیبانی نمیکنند. بسته simd در حال حاضر از AVX، AVX2 و AVX512 در amd64، NEON در arm64 و دستورالعملهای SIMD Wasm پشتیبانی میکند.
معماری SIMD در ابعاد مختلف متفاوت است. برخی از آنها یک اندازه برداری ثابت (wasm، PowerPC و s390x، 128 بیت) را ارائه می دهند. برخی چندین اندازه برداری ثابت را ارائه می دهند (amd64، با 128، 256، و 512؛ loong64 با 128 و 256). Riscv64 از بردارهایی با اندازه نامشخص بین 128 و 65536 بیت پشتیبانی می کند، اگرچه طول آن به توان های 2 محدود می شود. Arm64 از یک اندازه ثابت (128 بیت، NEON) و یک اندازه متغیر (128-2048 بیت، فقط قدرت های دو، SVE) پشتیبانی می کند.
در یک نمونه معین از یک معماری خاص، تعیین اینکه آن نمونه خاص از چه اندازه هایی پشتیبانی می کند نیاز به بررسی ویژگی دارد: amd64، اما آیا AVX، AVX2 یا AVX512 است؟ Arm64، اما NEON است یا SVE؟ اگر SVE، چقدر بزرگ است؟ کدام نوع SVE: SVE، SVE2، یا SVE2.1؟
معماری های مختلف SIMD در نحوه مدیریت پوشش برداری با یکدیگر متفاوت هستند. برای بردارها، if-then-else در سراسر یک بردار را می توان با ماسک پیاده سازی کرد. عملیات را انجام دهید، اما فقط نتیجه (یا بارگیری، یا ذخیره) را در جایی که ماسک "درست" است اختصاص دهید. برخی از انواع SIMD ماسک ارائه نمی دهند. همه عملیات در تمام عناصر کار می کنند و "ماسک" با بیت ماسک های برداری و عملیات بولی برداری (wasm، AVX، AVX2، NEON) انجام می شود. برخی رجیسترهای ماسک ویژه ای را ارائه می کنند که یک بیت عملیات حاکم بر یک عنصر برداری (AVX512 و RVV) دارد.
سایرین (SVE) یک بیت را به هر بایت برداری اختصاص می دهند، اما بیت کم اهمیت ترین بیت های ماسک هر عنصر، عملیات پوشانده شده را کنترل می کند. AVX2 همچنین از بارهای پوشانده و ذخیرهسازی پشتیبانی میکند، اما با استفاده از یک بردار ساده به عنوان ماسک، و با مهمترین بیت حاکم بر عملیات.
منبع سوم تنوع در خود عملیات است. هر معماری اصول اولیه خود را برای تنظیم مجدد عناصر برداری فراهم می کند. برخی نیاز به ورودی های ثابت دارند، برخی دیگر از ورودی های متغیر پشتیبانی می کنند. معماریهای مختلف SIMD از عملیاتهای مختلف مرتبط با رمزنگاری پشتیبانی میکنند. حتی محاسبات پایه می تواند پشتیبانی متفاوتی داشته باشد. برای مثال wasm فاقد مقایسه برای بردارهای اعداد صحیح 64 بیتی است. حتی برای یک طول برداری معین در یک معماری خاص، پشتیبانی دستورالعمل به «ویژگیهایی» بستگی دارد که باید بررسی شوند.
اگرچه بسته archsimd وابسته به معماری Go به گونهای طراحی شده بود که تا حد امکان در معماریها یکنواخت باشد، بسیاری از این ویژگیها باقی میمانند و طراحی، نوشتن و تست کد برای SIMD چند پلتفرمی را سخت میکنند. ما میتوانیم در بسته archsimd کارهای بیشتری انجام دهیم تا معماریهای مختلف شبیهتر به نظر برسند، اما فقط میتوانیم بدون به خطر انداختن کارایی، تا آنجا پیش برویم.
بسته جدید simd با حذف بردارهای با اندازه ثابت از سیستم نوع، و تنها با پشتیبانی از عملیاتی که در تقاطع همه پلتفرمهای مختلف قرار دارند، این تفاوتها را پنهان میکند و شکافهای تقاطع را با شبیهسازی کارآمد از نظر سایر دستورالعملهای SIMD پر میکند. هدف مجموعه ای از عملیات است که می باشد
در پلتفرمهایی که فاقد دستورالعملهای SIMD هستند یا فاقد پشتیبانی در archsimd هستند، همه عملیات شبیهسازی میشوند، بنابراین کد نوشته شده با استفاده از بسته simd همیشه اجرا میشود.
برای استفاده از این بسته آزمایشی، GOEXPERIMENT=simd را در زمان ساخت تنظیم کنید، درست مانند استفاده از بسته آزمایشی archsimd.
انواع بردار simd فقط با حروف بزرگ، جمع، انواع اولیه هستند، برای مثال simd.Uint8s یا simd.Float32s. بردارها از برش ها بارگیری و ذخیره می شوند، به عنوان مثال:
این مثال همچنین یکی از محدودیت های اولین نسخه آزمایشی این بسته را نشان می دهد. از آنجایی که هیچ روش مشترکی برای جمع کردن تمام عناصر یک بردار وجود ندارد، در Go 1.27 توسط simd پشتیبانی نمیشود، اگرچه ReduceSum در نسخه بعدی ظاهر میشود، بنابراین میتوان sum را با simd.ReduceSum جایگزین کرد.
مقایسههای SIMD مقادیر ماسکی را تولید میکنند که مخصوص عرض عنصر بردار متناظر است، به طوری که مقایسههای Int8s Mask8 و غیره را تولید میکنند و از مقادیر ماسک میتوان برای انتخاب و فیلتر کردن بردارها استفاده کرد.
در این جدول، V و U انواع برداری، M نوع ماسک، E یک نوع اسکالر و W یک عرض است.
ممکن است این اتفاق بیفتد که بسته simd برای تمام بخشهای یک برنامه خاص بسیار محدود است، یا اینکه ما هنوز یک شبیهسازی کافی برای برخی از ویژگیهای ضروری ارائه نکردهایم. برای این مورد، بسته simd از انتقال به و از SIMD خاص معماری پشتیبانی می کند. هر نوع برداری در بسته simd دارای یک روش تبدیل ()ToArch است که هر یک را برمی گرداند. که هر یک را می توان به یکی از انواع معماری خاص برای یک پلتفرم اطلاق کرد. برای تبدیل مجدد، از یکی از توابع simd استفاده کنید.<SimdType>FromArch.
برای کدهای قابل حمل، این الزام برای نوشتن کدهای معماری خاص برای هر یک از پلتفرم ها، از جمله یک شبیه سازی، ایجاد می کند.
در اینجا یک مثال کامل برای یک متد/تابع وجود دارد که در حال حاضر وجود ندارد، اما باید در Go 1.28 اضافه شود. فرض کنید الگوریتم شما به Int8s.OnesCount() نیاز دارد (که simd در Go 1.27 فاقد آن است). به جای بازنویسی کل الگوریتم برای هر پلتفرم، می توان فقط عملیات گمشده را پیاده سازی کرد.
اول، برای amd64، که فاقد دستورالعمل AVX و AVX2 است، اما نه AVX512:
تبدیل رابط و سوئیچ نوع به نظر می رسد که باید ناکارآمد باشند، اما اجرای سمت کامپایلر simd کد را تخصصی می کند و سوئیچ نوع را بهینه می کند.
NEON و Wasm هر دو از Int8s.OnesCount() پشتیبانی می کنند، بنابراین پیاده سازی آنها بسیار ساده تر است، اگرچه هنوز از Int8s.ToArch و Int8sFromArch استفاده می کند.
فراموش نکنید که برخی از افراد پشتیبانی سخت افزاری SIMD را ندارند:
و برای تکمیل تمرین، یک تابع شبیهسازی جداگانه بهعنوان بازگشتی در همه پیادهسازیها به اشتراک گذاشته شده است:
هر عملیاتی که پکیج simd ارائه می دهد باید در اکثر معماری ها به خوبی اجرا شود. به عنوان اولین قدم، هر عملیاتی که در همه جا پشتیبانی می شود، می تواند به راحتی در simd پشتیبانی شود. این شامل بارها، ذخیره ها، محاسبات و مقایسه ها می شود (اما نه همه مقایسه ها!).
یک تقاطع ساده بین روشهای SIMD از معماریهای مختلف هنوز هم حفرههای زیادی بر جای میگذارد. اینها با افزودن شبیهسازیها به APIهای مختلف archsimd خاص معماری پر میشوند. این API ها در حال حاضر شامل بسیاری از شبیه سازی های بی اهمیت برای ساده کردن زندگی برنامه نویسان Go هستند. جمع اعداد صحیح امضا شده و بدون علامت از دستورالعمل یکسانی استفاده می کنند، اما به همان روشی که Go از عملگر + برای int و uint پشتیبانی می کند، بسته archsimd هر دو Int8x16.Add(Int8x16) و Uint8x16.Add(Uint8x16) را ارائه می دهد، حتی اگر آن ها با یک دستورالعمل کامپایل شوند.
زبان های برنامه نویسی مدرن همچنین از برنامه نویسان انتظار ندارند که بدانند چگونه نفی ممیز شناور و قدر مطلق را با کمانچه بیتی پیاده سازی کنند، بنابراین archsimd آن را در صورت لزوم پیاده سازی می کند، یا اگر به آن نگاه کنید، آن را شبیه سازی می کند.
شبیه سازی های زیادی وجود دارد که فقط به 2 یا 3 دستورالعمل نیاز دارند. به عنوان مثال، برخی از معماریها تنها از یک فاصله شیفت با همان مقدار در عناصر برداری پشتیبانی میکنند، در حالی که برخی دیگر از یک فاصله تغییر متفاوت برای هر عنصر برداری پشتیبانی میکنند. برای پشتیبانی از جابجایی اسکالر در simd، ما تغییر اسکالر را با شیفت برداری شبیهسازی میکنیم. برخی از معماریها فاقد برخی مقایسههای بدون علامت هستند – اینها فقط مقایسه علامتدار هستند، به علاوه دو XOR با یک ثابت.
همه دستورالعمل های از دست رفته به این سادگی نیستند. دستورالعمل «ضریب حمل بدون حمل» برای رمزنگاری و جمعبندی چک CRC مهم است، اما همیشه پشتیبانی نمیشود. کنار گذاشتن آن از simd API از استفاده از آن برای برخی از الگوریتم های مهم جلوگیری می کند. بنابراین، ما یک شبیهسازی ارائه میکنیم، و چون یکی از کاربردهای مهم آن در کریپتو است، زمان اجرای آن بسته به ورودیهای آن متفاوت نیست.
در موارد دیگر، بهجای اجرای یک دستورالعمل ابتدایی مانند «افزودن جفتها» (که «افزودن افقی» نیز نامیده میشود)، برای بسته simd در نسخه بعدی، عملیات سطح بالاتری را ارائه میکنیم که معمولا از افزودن جفتها استفاده میشود، که کاهش مجموع است. این همچنین به محافظت کاربران از وابستگی به طول بردار کمک می کند. حتی با توجه به دستورالعمل سخت افزاری برای اضافه کردن جفت ها، تعداد مراحل کاهش به طول بردار بستگی دارد.
محدودیت پشتیبانی از همه پلتفرمها، از جمله پلتفرمهایی که پیشبینی میکنیم ظرف یک سال آینده در archsimd ظاهر میشوند، رویکردی محافظهکارانه را مجبور میکند که کدام روشها را به simd اضافه کنیم. Riscv64، ppc64، s390x، و loong64 همگی پسوندهای SIMD خود را دارند.
در پلتفرمهایی که پشتیبانی سختافزاری وجود دارد، رفتار را میتوان با GODEBUG تغییر داد تا آزمایش کد با استفاده از simd با پیکربندیهای سختافزاری مختلف آسانتر شود. می توانید متغیر محیطی GODEBUG را قبل از اجرای برنامه خود تنظیم کنید.
در Go 1.27، سطوح پشتیبانی SIMD تقریبا با طول برداری توصیف میشوند:
اگر کدهایی را که از simd استفاده می کند اشکال زدایی می کنید، یا حتی فقط به یک stack trace نگاه می کنید، متوجه انواع و روش های اضافی عجیب و غریب خواهید شد. دلیل آن این است که simd هم یک بسته است، هم یک بسته پیاده سازی داخلی و هم مقداری بازنویسی AST در قسمت جلویی کامپایلر.
بازنویسی AST کپی های تخصصی متعددی از توابع، متغیرها و انواعی ایجاد می کند که انواع simd را ذکر می کنند، که در آن انواع simd با ارجاع به انواع اندازه تخصصی در simd/internal/bridge جایگزین می شوند. هر یک از این انواع پل به عنوان یک نوع archsimd تعریف شده است، اما با مجموعه ای محدود از روش ها. توابع، متغیرها و انواع تخصصی پسوندی به شکل @simdNNN دریافت میکنند، که در آن NNN یا طول برداری (128، 256، یا 512) یا 0 است که نشاندهنده تقلید است.
توابعی که simd را به صورت داخلی ذکر میکنند، اما نه در امضای خود، به بستهبندیهایی تبدیل میشوند که سطح SIMD شناسایی شده در شروع برنامه را روشن میکنند و نسخه تخصصی مناسب آن عملکرد را فراخوانی میکنند. توابع تخصصی دیگر توابع تخصصی را مستقیما بدون سربار ارسال (و شاید با درون خطی) فراخوانی می کنند. این استراتژی بازنویسی به عنوان مصالحه ای بین تکرار کد و عملکرد SIMD انتخاب شد. سربار به اندازه لازم بالا می رود تا از ارسال در محاسبات SIMD جلوگیری شود، اما نه بالاتر.
اگر ارسال SIMD در یک محاسبات "خیلی کم" به نظر می رسد، ذکر بی رویه یک نوع simd آن را به سمت بالا حرکت می دهد، مانند این مثال:
ما قصد داریم به زودی یک پست وبلاگی منتشر کنیم که archsimd را با جزئیات بیشتر توصیف کند.
برای Go 1.28، ما قصد داریم پشتیبانی SVE را به archsimd اضافه کنیم و همچنین امیدواریم آن را به simd اضافه کنیم. مهمتر از آن، امیدواریم عملیات SIMD اضافی را به مواردی که بسته simd قبلا پشتیبانی می کند اضافه کنیم (به عنوان مثال، OnesCount، عملیات ماسک، عملیات کاهش، عملیات درهم ریختن برداری). Go 1.28 همچنین شامل تعداد کمی از «انواع ویژگی» میشود تا از کاهش تا حد امکان به شبیهسازی کامل برای پلتفرمهایی که پیادهسازی بردار سختافزاری دارند اما فاقد یک یا چند عملیات هستند، مانند Raspberry Pi، جلوگیری شود.
متن اصلی (انگلیسی)
Platform-Independent SIMD in Go
Article URL: https://go.dev/blog/simd-experiment Comments URL: https://news.ycombinator.com/item?id=49843269 Points: 238 # Comments: 86