سخت افزار درونی در NET 8.

سخت افزار درونی در NET 8.
فهرست مقاله [نمایش]


    .NET تاریخچه طولانی در ارائه دسترسی به قابلیت‌های سخت‌افزاری اضافی از طریق APIهایی دارد که به طور بومی توسط کامپایلر JIT درک می‌شوند. این امر از .NET Framework در سال 2014 آغاز شد و با معرفی .NET Core 3.0 در سال 2019 گسترش یافت. از آن زمان، ران‌تایم به طور تکراری APIهای بیشتری ارائه داده و در هر نسخه از این موضوع بهتر بهره برده است.
    پشتیبانی از  WebAssembly
     WebAssembly، یا به اختصار Wasm، در واقع کدی است که در مرورگر شما اجرا می‌شود و اجازه می‌دهد تا یک پروفایل عملکردی بسیار بالاتر از پشتیبانی اسکریپت‌های معمول تفسیر شده داشته باشد. به عنوان یک پلتفرم، Wasm شروع به ارائه پشتیبانی زیرین SIMD (دستورالعمل واحد، داده‌های چندگانه) کرده است تا الگوریتم‌های اصلی را تسریع کند و .NET به تبع آن تصمیم گرفته است تا از این عملکرد از طریق intrinsics سخت‌افزاری پشتیبانی کند.
    این پشتیبانی بسیار شبیه به مبانی است که سایر پلتفرم‌ها ارائه می‌دهند و بنابراین ما به جزئیات قابل توجهی وارد نخواهیم شد. در عوض، می‌توانید انتظار داشته باشید که الگوریتم‌های چندپلتفرمی موجود شما با استفاده از Vector128<T> به طور ضمنی در جایی که پشتیبانی می‌شود، فعال شوند. اگر می‌خواهید از عملکردهای منحصر به فرد Wasm به صورت مستقیم استفاده کنید، می‌توانید از APIهایی که توسط کلاس‌های PackedSimd و WasmBase در فضای نام System.Runtime.Intrinsics.Wasm ارائه می‌شوند، استفاده کنید

     پشتیبانی از AVX-512

    AVX-512 یک مجموعه ویژگی جدید برای کامپیوترهای x86 و x64 است. این همراه با خود مجموعه‌ای بزرگ از دستورالعمل‌ها و عملکردهای سخت‌افزاری جدیدی را به همراه دارد که قبلاً در دسترس نبودند، از جمله پشتیبانی برای 16 رجیستر SIMD اضافی، ماسکینگ اختصاصی و کار بر روی 512 بیت داده در یک زمان. دسترسی به این عملکرد نیازمند یک پردازنده نسبتاً جدید است، یعنی نیازمند Skylake-X یا جدیدتر از اینتل و Zen4 یا جدیدتر از AMD است. به دلیل این، تعداد کاربرانی که می‌توانند از این عملکرد جدید بهره ببرند کمتر است، اما بهبودهایی که می‌تواند به این سخت‌افزار بیاورد همچنان قابل توجه است و ارائه پشتیبانی از آن برای بارهای کاری سنگین داده ارزشمند است. علاوه بر این، JIT به صورت فرصت‌طلبانه از این دستورالعمل‌ها برای کد SIMD موجود استفاده می‌کند جایی که فایده‌ای را تشخیص می‌دهد. برخی مثال‌ها عبارتند از:

    •    استفاده از vpternlog به جای and، andn، یا or وقتی یک انتخاب شرطی بیتی انجام می‌شود (Vector128.ConditionalSelect)
    •    استفاده از کدگذاری EVEX برای انجام بیشتر عملیات در کمتر بایت‌های کد، مانند برای پخش داخلی (x + Vector128.Create(5))
    •    استفاده از دستورالعمل‌های جدیدتر جایی که پشتیبانی اکنون با AVX-512 وجود دارد، مانند برای ترکیب کامل عرض و بسیاری از عملیات‌های بلند/ulong (Int64/UInt64)
    •    بهبودهای دیگری نیز وجود داشت، که در اینجا لیست نشده‌اند، و می‌توان انتظار داشت که بیشتری به مرور زمان اضافه شوند

     برخی موارد مانند Vector<T> که اجازه می‌دهند به 512 بیت مقیاس‌پذیری کنند، در .NET 8 تکمیل نشده بودند
    برای پشتیبانی از اندازه بردار جدید 512 بیت، .NET نوع Vector512<T> را معرفی کرد. این همان سطح API عمومی را که انواع بردار ثابت‌اندازه مانند Vector256<T> دارند، ارائه می‌کند. همچنین ادامه می‌دهد به نمایش دادن ویژگی Vector512.IsHardwareAccelerated که به شما اجازه می‌دهد تعیین کنید آیا منطق عمومی باید در سخت‌افزار تسریع شود یا اینکه در نهایت از طریق یک فالبک نرم‌افزاری رفتار را شبیه‌سازی خواهد کرد.

    Vector512 به طور پیش‌فرض با AVX-512 در سخت‌افزار Ice Lake و جدیدتر تسریع می‌شود (و در نتیجه Vector512.IsHardwareAccelerated گزارش می‌دهد که صحیح است)، جایی که دستورالعمل‌های AVX-512 باعث کاهش چشمگیر سرعت CPU نمی‌شوند؛ در حالی که استفاده از دستورالعمل‌های AVX-512 می‌تواند کاهش سرعت قابل توجهی را در سخت‌افزارهای مبتنی بر Skylake-X، Cascade Lake، و Cooper Lake ایجاد کند (همچنین بخش 2.5.3 مدیریت توان سرور Skylake در دستورالعمل بهینه‌سازی معماری‌های Intel® 64 و IA-32 را مشاهده کنید: جلد 1). در حالی که این در نهایت برای بارهای کاری بزرگ مفید است، می‌تواند تأثیر منفی بر بارهای کاری کوچکتر داشته باشد و به همین دلیل ما به طور پیش‌فرض گزارش می‌دهیم که Vector512.IsHardwareAccelerated بر این پلتفرم‌ها نادرست است. Avx512F.IsSupported همچنان گزارش می‌دهد که صحیح است و پیاده‌سازی زیرین Vector512 همچنان از دستورالعمل‌های AVX-512 استفاده می‌کند اگر به طور مستقیم فراخوانی شود. این امکان را به بارهای کاری می‌دهد تا از عملکرد بهره‌برداری کنند جایی که می‌دانند صریحاً مفید است بدون اینکه به طور تصادفی تأثیر منفی بر دیگران داشته باشند.

    تشکر ویژه

    این عملکرد از طریق مشارکت‌های قابل توجه توسط دوستان ما در اینتل امکان‌پذیر شد. تیم .NET و اینتل بارها در طول سال‌ها همکاری کرده‌اند و این همکاری ادامه یافته است تا ما با هم بر روی طراحی کلی و پیاده‌سازی کار کنیم، که اجازه داد پشتیبانی AVX-512 در .NET 8 قرار گیرد.
    همچنین مشارکت و تأیید زیادی از جامعه .NET وجود داشت که به دستیابی به موفقیت و بهبود بیشتر نسخه کمک کرد.
    اگر می‌خواهید مشارکت کنید یا نظر بدهید، لطفاً به ما در مخازن dotnet/runtime در GitHub بپیوندید، و به بررسی API در کانال YouTube بنیاد .NET گوش دهید که ما در آن برنامه‌های جدید اضافه شده به کتابخانه‌های .NET را بحث می‌کنیم و حتی می‌توانید نظرات خود را از طریق کانال چت ارائه دهید. 

    فقط 512 بیت نیست؟

    بر خلاف نام، AVX-512 فقط در مورد پشتیبانی 512 بیتی نیست. رجیسترهای اضافی، پشتیبانی از ماسک، پشتیبانی از گرد کردن یا پخش داخلی و دستورالعمل‌های جدید نیز همه برای بردارهای 128 بیتی و 256 بیتی وجود دارند. این بدان معناست که بارهای کاری موجود شما به طور ضمنی بهتر می‌شوند و شما می‌توانید از عملکردهای جدیدتر به طور صریح استفاده کنید که چنین روشنایی ضمنی در آن‌ها امکان‌پذیر نیست.
    وقتی SSE در سال 1999 بر روی اینتل Pentium III معرفی شد، 8 رجیستر را ارائه داد که هر کدام 128 بیت طول داشتند. این رجیسترها به عنوان xmm0 تا xmm7 شناخته می‌شدند. وقتی بعداً پلتفرم x64 در سال 2003 بر روی AMD Athlon 64 معرفی شد، 8 رجیستر اضافی را ارائه داد که در کد 64 بیتی قابل دسترسی بودند. این رجیسترها به نام xmm8 تا xmm15 نامگذاری شدند. این پشتیبانی اولیه از یک طرح کدگذاری ساده استفاده می‌کرد که به شیوه‌ای بسیار مشابه با دستورالعمل‌های عمومی کار می‌کرد و تنها اجازه می‌داد که 2 رجیستر مشخص شوند. برای چیزی مانند جمع که نیاز به 2 ورودی دارد، این بدان معنا بود که یکی از رجیسترها به عنوان هم ورودی و هم خروجی عمل می‌کرد. این بدان معنا بود که اگر ورودی و خروجی شما باید متفاوت باشد، برای تکمیل عملیات به 2 دستورالعمل نیاز داشتید. به طور مؤثر، z = x + y شما تبدیل می‌شد به z = x؛ z += y. در سطح بالا این‌ها به همان شکل رفتار می‌کنند، اما در سطح پایین 2 مرحله به جای 1 مرحله برای اتفاق افتادن وجود دارد.

    این سپس در سال 2011 با معرفی AVX توسط اینتل در پردازنده‌های مبتنی بر Sandy Bridge گسترش یافت و پشتیبانی آن به 256 بیت افزایش یافت. این رجیسترهای جدیدتر به نام‌های ymm0 تا ymm15 نام‌گذاری شدند، با این تفاوت که تنها رجیسترها تا ymm7 برای کد 32 بیتی قابل دسترس بودند. این همچنین یک کدگذاری جدید به نام VEX (توسعه‌های برداری) را معرفی کرد که اجازه می‌داد 3 رجیستر کدگذاری شوند. این به این معنی بود که می‌توانید z = x + y را به طور مستقیم کدگذاری کنید و نیازی به تقسیم آن به 2 مرحله جداگانه نباشد.
    سپس AVX-512 در سال 2017 توسط اینتل با پردازنده‌های مبتنی بر Skylake-X معرفی شد. این پشتیبانی را به 512 بیت گسترش داد و رجیسترها را به نام‌های zmm0 تا zmm15 نام‌گذاری کرد. همچنین 16 رجیستر جدید، به درستی با نام‌های zmm16 تا zmm31 نام‌گذاری شدند که همچنین دارای نسخه‌های xmm16-xmm31 و ymm16-ymm31 هستند. مانند موارد قبلی، تنها رجیسترها تا zmm7 برای کد 32 بیتی قابل دسترس هستند. این 8 رجیستر جدید، به نام‌های k0 تا k7 را معرفی کرد که برای پشتیبانی از «ماسکینگ» طراحی شده‌اند و همچنین یک کدگذاری جدید به نام EVEX (توسعه‌های برداری پیشرفته) که اجازه می‌دهد تمام این اطلاعات جدید بیان شوند. کدگذاری EVEX همچنین دارای ویژگی‌های دیگری است که اجازه می‌دهد اطلاعات و عملیات رایج‌تر به شکلی فشرده‌تر بیان شوند. این می‌تواند به کاهش اندازه کد کمک کند در حالی که عملکرد را بهبود می‌بخشد.

    چه دستورالعمل‌های جدیدی وجود دارند؟

    عملکردهای جدید زیادی وجود دارد، خیلی زیاد که نمی‌توان همه را در این پست وبلاگ پوشش داد. اما برخی از دستورالعمل‌های جدید قابل توجه شامل موارد زیر است:
    •    پشتیبانی برای انجام عملیاتی مانند Abs، Max، Min و شیفتینگ روی اعداد صحیح 64 بیتی - قبلاً این عملکرد باید با استفاده از چندین دستورالعمل شبیه‌سازی می‌شد
    •    پشتیبانی برای انجام تبدیل‌ها بین اعداد صحیح بدون علامت و انواع نقطه شناور
    •    پشتیبانی برای کار با موارد لبه نقطه شناور
    •    پشتیبانی برای بازچینی کامل عناصر در یک بردار یا چندین بردار
    •    پشتیبانی برای انجام 2 عملیات بیتی در یک دستورالعمل واحد

    پشتیبانی از اعداد صحیح 64 بیتی قابل توجه است زیرا به این معنی است که کار با داده‌های 64 بیتی نیازی به استفاده از یک دنباله کد کندتر یا جایگزین برای پشتیبانی از همان عملکرد ندارد. این کار را برای نوشتن کد شما بسیار آسان‌تر می‌کند و انتظار می‌رود که رفتار آن بدون توجه به نوع داده زیرینی که با آن کار می‌کنید، یکسان باشد.
    پشتیبانی از تبدیل نقطه شناور به اعداد صحیح بدون علامت به دلایل مشابه قابل توجه است. تبدیل از double به long به یک دستورالعمل نیاز داشت، اما تبدیل از double به ulong به بسیاری از دستورالعمل‌ها نیاز داشت. با AVX-512 این به یک دستورالعمل تبدیل می‌شود و به کاربران امکان می‌دهد تا عملکرد مورد انتظار را هنگام کار با داده‌های بدون علامت به دست آورند. این می‌تواند در سناریوهای مختلف پردازش تصویر یا یادگیری ماشینی رایج باشد.
    پشتیبانی گسترده برای داده‌های نقطه شناور یکی از ویژگی‌های مورد علاقه من در AVX-512 است. برخی از مثال‌ها شامل توانایی استخراج توان بدون جانبداری (Avx512F.GetExponent) یا مانتیسا نرمال‌شده (Avx512F.GetMantissa)، گرد کردن یک مقدار نقطه شناور به تعداد خاصی از بیت‌های کسری (Avx512F.RoundScale)، ضرب کردن یک مقدار در 2^x (Avx512F.Scale، شناخته شده در C به عنوان scalebn)، انجام Min، Max، MinMagnitude و MaxMagnitude با پردازش صحیح +0 و -0 (Avx512DQ.Range)، و حتی انجام کاهش‌ها که در هنگام کار با مقادیر بزرگ برای توابع مثلثاتی مانند Sin یا Cos مفید است (Avx512DQ.Reduce) است.
    با این حال، یکی از موارد مورد علاقه شخصی من دستورالعملی به نام vfixupimm (Avx512F.Fixup) است. در سطح بالا، این دستورالعمل به شما اجازه می‌دهد تا تعدادی از موارد لبه ورودی را تشخیص دهید و خروجی را به یکی از خروجی‌های معمول "تعمیر" کنید و این کار را به ازای هر عنصر انجام دهید. این می‌تواند عملکرد برخی الگوریتم‌ها را به طور چشمگیری بهبود بخشد و میزان پردازش مورد نیاز را به شدت کاهش دهد. نحوه عملکرد آن این است که ابتدا 4 ورودی به نام‌های چپ، راست، جدول و کنترل را می‌گیرد. ابتدا ارزیابی مقدار نقطه شناور در راست انجام می‌دهد و تعیین می‌کند که آیا QNaN (0)، SNaN (1)، +/-0 (2)، +1 (3)، -Infinity (4)، +Infinity (5)، منفی (6) یا مثبت (7) است. سپس از آن برای خواندن 4 بیت از جدول استفاده می‌کند (QNaN که 0 است، بیت‌های 0..3 را می‌خواند؛ منفی که 6 است بیت‌های 24..27 را می‌خواند). مقدار این 4 بیت در جدول تعیین می‌کند که نتیجه چه خواهد بود. نتایج احتمالی (به ازای هر عنصر) عبارتند از:

    الگوی بیتتعریف
    0b0000left[i]
    0b0001right[i]
    0b0010QNaN(right[i])
    0b0011QNaN
    0b0100-Infinity
    0b0101+Infinity
    0b0110IsNegative(right[i]) ? -Infinity : +Infinity
    0b0111-0.0
    0b1000+0.0
    0b1001-1.0
    0b1010+1.0
    0b1011+0.5
    0b1100+90.0
    0b1101PI / 2
    0b1110MaxValue
    0b1111MinValue

     

    با SSE، پشتیبانی برای ترتیب‌بندی مجدد داده‌ها در یک بردار وجود داشت. به عنوان مثال، فرض کنید 0، 1، 2، 3 را دارید و می‌خواهید آن را به ترتیب 3، 1، 2، 0 مرتب کنید. با معرفی AVX و گسترش به 256 بیت، این پشتیبانی نیز گسترش یافت. با این حال، به دلیل نحوه عملکرد دستورالعمل‌ها، شما در واقع همان عملیات 128 بیتی را دو بار انجام می‌دادید. این کار توسعه الگوریتم‌های موجود به 256 بیت را آسان کرد، زیرا شما در واقع همان کار را دو بار انجام می‌دهید. با این حال، کار با سایر الگوریتم‌ها را دشوارتر می‌کرد، وقتی که شما واقعاً نیاز داشتید تا کل بردار را به طور یکپارچه در نظر بگیرید. برخی دستورالعمل‌ها بودند که به شما امکان می‌دادند داده‌ها را در کل بردار 256 بیتی مرتب کنید، اما آن‌ها اغلب محدود بودند یا در نحوه مرتب‌سازی داده‌ها یا در انواعی که پشتیبانی می‌کردند (ترکیب کامل عناصر بایت یک مثال قابل توجه از پشتیبانی موجود نیست). AVX-512 بسیاری از همان ملاحظات را برای پشتیبانی گسترده 512 بیتی خود دارد. با این حال، همچنین دستورالعمل‌های جدیدی را معرفی می‌کند که شکاف را پر می‌کنند و اکنون به شما اجازه می‌دهند تا عناصر را برای هر اندازه عنصر به طور کامل مرتب کنید.
    در نهایت، یکی دیگر از موارد مورد علاقه شخصی من دستورالعملی به نام vpternlog (Avx512F.TernaryLogic) است. این دستورالعمل به شما اجازه می‌دهد هر 2 عملیات بیتی را ترکیب کنید، به طوری که آن‌ها می‌توانند در یک دستورالعمل اجرا شوند. به عنوان مثال، شما می‌توانید (a & b) | c را انجام دهید. نحوه کار این است که 4 ورودی، a، b، c و کنترل را می‌گیرد. سپس شما 3 کلید را به خاطر دارید: A: 0xF0، B: 0xCC، C: 0xAA. برای نمایش عملیات مورد نظر، شما به سادگی کنترل را با انجام آن عملیات بر روی این کلیدها ایجاد می‌کنید. بنابراین، اگر می‌خواستید فقط a را برگردانید، از 0xF0 استفاده می‌کنید. اگر می‌خواستید a & b را انجام دهید، از (byte)(0xF0 & 0xCC) استفاده می‌کنید. اگر می‌خواستید (a & b) | c را انجام دهید، آنگاه (byte)((0xF0 & 0xCC) | 0xAA است. در مجموع 256 عملیات مختلف ممکن است، با بلوک‌های اساسی این کلیدها و عملیات‌های بیتی زیر:
     

     

    عملیاتتعریف
    not~x
    andx & y
    nand~x & y
    orیا y
    nor~x یا y
    xorx ^ y
    xnor~x ^ y

     

    عملیات‌های ویژه‌ای نیز وجود دارند که با استفاده از عملیات‌های پایه‌ای فوق، می‌توانند گسترش

     

    عملیاتتعریف
    falseالگوی بیتی 0x00
    trueالگوی بیتی 0xFF
    majorاگر دو یا بیشتر از دو بیت ورودی 0 باشند، 0 برمی‌گرداند؛ اگر دو یا بیشتر از دو بیت ورودی 1 باشند، 1 برمی‌گرداند
    minorاگر دو یا بیشتر از دو بیت ورودی 1 باشند، 0 برمی‌گرداند؛ اگر دو یا بیشتر از دو بیت ورودی 0 باشند، 1 برمی‌گرداند
    انتخاب شرطیبه صورت منطقی (x & y)


    در .NET 8 ما پشتیبانی کامل برای شناسایی و تا کردن این الگوها به صورت خودکار برای ایجاد vpternlog را به اتمام نرساندیم. انتظار داریم این در .NET 9 به نمایش درآید.

    پشتیبانی از ماسکینگ (Masking) چیست؟

    در ساده‌ترین سطح، نوشتن کد برداری (vectorized code) شامل استفاده از SIMD برای انجام یک عملیات اساسی یکسان روی تعداد مختلف المان‌های نوع T در یک دستورالعمل واحد است. این زمانی بسیار خوب عمل می‌کند که همان عملیات باید روی همه داده‌ها انجام شود. با این حال، همه داده‌ها لزوما یکنواخت نیستند و گاهی اوقات نیاز است که ورودی‌های خاصی را به طور متفاوتی رسیدگی کنید. برای مثال، ممکن است بخواهید عملیات متفاوتی برای اعداد مثبت در مقابل اعداد منفی انجام دهید. ممکن است نیاز داشته باشید نتیجه متفاوتی برگردانید اگر کاربر NaN وارد کرده باشد، و غیره. هنگام نوشتن کد معمولی، معمولاً این موارد را با یک شاخه (branch) رسیدگی می‌کنید و این بسیار خوب عمل می‌کند. اما، هنگام نوشتن کد برداری، چنین شاخه‌هایی قابلیت استفاده از دستورالعمل‌های SIMD را مختل می‌کنند چون باید هر عنصر را به طور مستقل رسیدگی کنید. .NET از این موقعیت‌ها در مکان‌های مختلف استفاده می‌کند، از جمله APIهای جدید TensorPrimitives که به ما اجازه می‌دهد داده‌های انتهایی را که در غیر این صورت در یک بردار کامل جا نمی‌گرفتند، رسیدگی کنیم.
    راه‌حل معمول برای این موضوع نوشتن کد «بدون شاخه» است. یکی از ساده‌ترین راه‌ها برای انجام این کار محاسبه هر دو پاسخ و سپس استفاده از عملیات‌های بیتی برای انتخاب پاسخ صحیح است. می‌توانید این را مانند یک شرط سه‌تایی cond ? result1 : result2 در نظر بگیرید. برای پشتیبانی از این در SIMD، API به نام ConditionalSelect وجود دارد که یک ماسک و هر دو نتیجه را می‌گیرد. ماسک نیز یک بردار است، اما مقادیر آن معمولا یا AllBitsSet یا Zero هستند. وقتی این الگو را دارید، پیاده‌سازی ConditionalSelect به طور موثر (cond & result1) | (~cond & result2) است. این به معنای گرفتن بیت‌ها از result1 است که بیت متناظر در cond 1 باشد و در غیر این صورت گرفتن بیت متناظر از result2 (وقتی بیت در cond 0 باشد). بنابراین اگر می‌خواستید تمام مقادیر منفی را به 0 تبدیل کنید، می‌توانید چیزی مانند (x < 0) ? 0 : x برای کد معمولی داشته باشید و Vector128.ConditionalSelect(Vector128.LessThan(x, Vector128.Zero), Vector128.Zero, x) برای کد برداری. این کمی طولانی‌تر است، اما می‌تواند بهبود قابل توجهی در عملکرد ارائه دهد.

    زمانی که سخت‌افزارها برای اولین بار از پشتیبانی SIMD بهره بردند، باید این ماسکینگ را به صورت کاملاً واقعی با انجام ۳ دستورالعمل: and، nand، و or پشتیبانی می‌کردید. با ورود سخت‌افزارهای جدیدتر، نسخه‌های بهینه‌تری اضافه شدند که به شما اجازه می‌دادند این کار را در یک دستورالعمل واحد انجام دهید، مانند blendv در x86/x64 و bsl در Arm64. AVX-512 سپس این را یک گام جلوتر برد و با معرفی پشتیبانی سخت‌افزاری اختصاصی برای بیان ماسک‌ها و ردیابی آن‌ها در رجیسترها (k0-k7 که قبلاً ذکر شد)، آن را توسعه داد. سپس پشتیبانی اضافی برای اجازه دادن به این ماسکینگ به عنوان بخشی از تقریباً هر عملیات دیگری فراهم شد. بنابراین به جای اینکه مجبور باشید vcmpltps؛ vblendvps؛ vaddps (مقایسه، ماسک، سپس اضافه کردن) را مشخص کنید، می‌توانستید آن ماسک را مستقیماً به عنوان بخشی از اضافه کردن کدگذاری کنید (و به این ترتیب vcmpltps؛ vaddps را به جای آن صادر کنید). این اجازه می‌دهد تا سخت‌افزار عملیات‌های بیشتری را در فضای کمتری نمایش دهد، تراکم کد را بهبود بخشد، و بهتر از رفتار مورد نظر بهره ببرد.
    قابل ذکر است که ما مفهوم ۱ به ۱ را با سخت‌افزار زیرین برای ماسکینگ در اینجا مستقیماً بروز نمی‌دهیم. بلکه JIT همچنان یک بردار معمولی را برای نتایج مقایسه می‌گیرد و برمی‌گرداند و الگوهای مربوطه را شناسایی کرده و سپس از ویژگی‌های ماسکینگ مبتنی بر این به صورت فرصت‌طلبانه روشنایی می‌دهد. این امکان را فراهم می‌کند که سطح API نمایش داده شده به طور قابل توجهی کوچک‌تر باشد (بیش از ۳۰۰۰ API کمتر)، برای کد موجود به طور عمده «فقط کار کند» و بدون اقدام صریح از پشتیبانی سخت‌افزار جدیدتر بهره ببرد، و برای کاربرانی که می‌خواهند از AVX-512 پشتیبانی کنند، نیازی به یادگیری مفاهیم جدید یا نوشتن کد به روش جدید نباشد.

    چه مثال‌هایی از استفاده عملی AVX-512 وجود دارد؟

    AVX-512 می‌تواند برای تسریع همه سناریوهایی که بر اساس SSE یا AVX هستند استفاده شود. یک روش آسان برای شناسایی مکان‌هایی که کتابخانه‌های .NET از این تسریع استفاده می‌کنند، جستجو برای مکان‌هایی است که ما Vector512.IsHardwareAccelerated را فراخوانی می‌کنیم. این کار را می‌توان با استفاده از source.dot.net انجام داد.
    مواردی که ما تسریع کرده‌ایم شامل:
    •    System.Collections.BitArray – ایجاد، عملیات‌های منطقی and، or، xor، not
    •    System.Linq.Enumerable – بیشترین (Max) و کمترین (Min)
    •    System.Buffers.Text.Base64 – رمزگشایی، رمزنگاری
    •    System.String – برابری (Equals)، بی‌تفاوتی نسبت به حروف بزرگ یا کوچک (IgnoreCase)
    •    System.Span – جستجوی عنصر (IndexOf)، جستجوی چند عنصر (IndexOfAny)، جستجو در بازه (IndexOfAnyInRange)، برابری توالی (SequenceEqual)، معکوس کردن (Reverse)، حاوی بودن (Contains) و غیره
    دیگر مثال‌ها در سراسر کتابخانه‌های .NET و اکوسیستم عمومی .NET وجود دارد، که شامل سناریوهایی مانند تبدیل رنگ، پردازش تصویر، یادگیری ماشینی، ترجمه متن، تجزیه JSON، رندرینگ نرم‌افزاری، پردازش اشعه (ray tracing)، تسریع بازی‌ها و بسیاری موارد دیگر می‌شود.

    بعد چی؟

    ما برنامه داریم که پشتیبانی از ویژگی‌های سخت‌افزاری در .NET را هر زمان و جایی که منطقی باشد، بهبود ببخشیم. لطفاً توجه داشته باشید که موارد زیر تفکرات آینده‌نگرانه و گمانه‌زنی هستند. این لیست کامل نیست و ما هیچ تضمینی نمی‌دهیم که هیچ‌کدام از این ویژگی‌ها ارائه شوند یا اگر ارائه شوند، چه زمانی منتشر خواهند شد.
    برخی از موارد در برنامه طولانی‌مدت ما شامل موارد زیر می‌شوند:

    •    پشتیبانی از SVE و SVE2 برای Arm64
    •    AVX10 برای x86/x64
    •    اجازه دادن به Vector<T> برای گسترش خودکار به 512 بیت
    •    یک رابط ISimdVector<TSelf, T> برای امکان استفاده بهتر از منطق SIMD
    ا    یک آنالیزور برای تشویق کاربران به استفاده از APIهای چندپلتفرمی که در آن‌ها معنی‌شناسی یکسان است (استفاده از x + y به جای Sse.Add(x, y))
    •    یک آنالیزور برای شناسایی الگوهایی که ممکن است جایگزین‌های بهینه‌تری داشته باشند (استفاده از value + value به جای value * 2 یا Sse.UnpackHigh(value, value) به جای Sse.Shuffle(value, value, 0b11_11_10_10))
    •    استفاده بیشتر و صریح از ویژگی‌های سخت‌افزاری در APIهای مختلف .NET
    •    APIهای چندپلتفرمی اضافی برای کمک به انتزاع عملیات‌های مشترک
    •    دریافت شاخص اولین/آخرین مطابقت در یک ماسک
    •    دریافت تعداد مطابقت‌ها در یک ماسک
    •    تعیین اینکه آیا مطابقت‌هایی وجود دارند
    •    اجازه دادن به رفتار غیرقطعی برای مواردی مانند Shuffle یا ConditionalSelect
    •    این APIها امروز در همه پلتفرم‌ها رفتار تعریف شده‌ای دارند، مانند Shuffle که هر شاخص خارج از محدوده را به عنوان صفر کردن عنصر مقصد تلقی می‌کند
    •    APIهای جدید، مانند ShuffleUnsafe، به جای آن اجازه رفتار متفاوتی برای شاخص‌های خارج از محدوده می‌دهند
    •    برای چنین سناریویی، Arm64 همان رفتار را خواهد داشت، در حالی که x64 فقط در صورت تنظیم بیت بالاترین ارزش دارد
    •    شناسایی الگوهای اضافی برای مواردی مانند
    •    ماسکینگ تعبیه شده (AVX512، AVX10، SVE/SVE2)
    •    ترکیب عملیات‌های منطقی (vpternlog در AVX512)
    •    فرصت‌های محدود تا کردن ثابت در زمان JIT
    اگر به دنبال استفاده از ویژگی‌های سخت‌افزاری در .NET هستید، تشویق می‌کنیم که APIهای موجود در فضای نام System.Runtime.Intrinsics را امتحان کنید، پیشنهادات API برای عملکردهایی که احساس می‌کنید گم شده‌اند یا می‌توانند بهبود یابند، ثبت کنید و در نسخه‌های پیش‌نمایش ما شرکت کنید تا قبل از ارسال آن‌ها از عملکرد آن‌ها استفاده کنید و کمک کنید تا هر انتشاری بهتر از آخرین باشد!

    اطلاعات نویسنده

    ارسال دیدگاه

    برای افزودن دیدگاه خود، نیاز است ابتدا وارد حساب کاربری‌تان شوید


    دیدگاه کاربران