دستور واحد، دادههای متعدد (SIMD) نوعی موازیسازی در سطح سختافزار است. همان محاسبه توسط پردازنده روی چندین عنصر بهطور همزمان انجام میشود.
برای مثال، جمع عنصربهعنصر دو آرایه از چهار عدد اعشاری ۳۲ بیتی را در نظر بگیرید.
یک پیادهسازی اسکالر چهار دستور addss جداگانه صادر میکند، یکی برای هر عنصر.
یک پیادهسازی SIMD از یک دستور addps استفاده میکند تا هر چهار جمع را بهطور موازی انجام دهد.
این یعنی چهار برابر کار در هر دستور.
در زبان اسمبلی x86-64، دستورهای SIMD معمولاً با استفاده از ثباتهای xmm انجام میشوند.
برخی از این دستورها روی اعداد صحیح کار میکنند.
برخی دیگر روی اعداد اعشاری.
به آن اعداد صحیح یا اعشاری بستهبندیشده میگویند. هر یک فضایی را در همان ثبات اشغال میکند که خط نامیده میشود. دستورهای SIMD معمولاً بهطور موازی روی مقادیری در همان موقعیت خط در دو عملوند مبدأ کار میکنند. نتیجهی هر عملیات در خطی با همان موقعیت در عملوند مقصد ذخیره میشود.
خط یک محدودیت مطلق نیست. بلکه تفسیری است که هر دستور به دنبالهای از بایتها میدهد. یک عملوند ۱۲۸ بیتی شامل ۱۶ بایت است. بسته به دستور، این بایتها میتوانند بهصورت ۱۶ خطِ ۱ بایتی، ۸ خطِ ۲ بایتی، ۴ خطِ ۴ بایتی، ۲ خطِ ۸ بایتی یا حتی یک خطِ ۱۶ بایتی تفسیر شوند.
برای مثال، دستور addps مقادیر اعشاری بستهبندیشدهی دقت یکگانه (۴ بایتی) را جمع میکند.
برای این دستور، هر تکهی ۴ بایتی در یک عملوند، خطی متفاوت است.
در یک عملوند ۱۲۸ بیتی (۱۶ بایتی):
| عملوند | خط چهارم | خط سوم | خط دوم | خط اول |
|---|---|---|---|---|
| مبدأ ۱ | ۱٫۵ | ۲٫۱ | ۶٫۲ | ۳٫۴ |
| + | + | + | + | + |
| مبدأ ۲ | ۰٫۵ | ۸٫۲۶ | -۱۳٫۴ | ۲۲٫۹ |
| = | = | = | = | = |
| نتیجه | ۲٫۰ | ۱۰٫۳۶ | -۷٫۲ | ۲۶٫۳ |
از سوی دیگر، یک عملوند ۱۲۸ بیتی در مورد mulpd بهصورت دارا بودن ۲ خط تفسیر میشود.
چون این دستور ضرب بستهبندیشده را روی مقادیر دقت مضاعف (۸ بایتی) انجام میدهد:
| عملوند | خط دوم | خط اول |
|---|---|---|
| مبدأ ۱ | ۴٫۲ | ۲٫۱ |
| * | * | * |
| مبدأ ۲ | ۱٫۰ | ۹۲٫۴ |
| = | = | = |
| نتیجه | ۴٫۲ | ۱۹۴٫۰۴ |
در این مفهوم روی گزیدهای کوچک از دستورهای SIMD تمرکز میکنیم.
آنها با استفاده از ثباتهای xmm روی مقادیر اعشاری بستهبندیشده کار میکنند.
بهعنوان یک قاعدهی سرانگشتی، دستورهای SIMD که روی مقادیر اعشاری کار میکنند، شکل دوعملوندی دارند. اسم آنها از ساختار مشخصی پیروی میکند:
add، mul و غیره).p برای نشان دادن اینکه دستور روی دادههای بستهبندیشده کار میکند، در همان موقعیتی که پیشتر s (برای دادههای اسکالر) قرار داشت.s برای دقت یکگانه و d برای دقت مضاعف).بنابراین اسم بستهبندیشده از اسم اسکالر به دست میآید، با جایگزین کردن s (اسکالر) در میان یادآور با p (بستهبندیشده).
اسم عملیات و پسوند اندازه بدون تغییر میمانند:
| اسکالر | بستهبندیشده | معنی |
|---|---|---|
addss |
addps |
افزودن، دقت یکگانه (خطهای ۳۲ بیتی) |
addsd |
addpd |
افزودن، دقت مضاعف (خطهای ۶۴ بیتی) |
mulss |
mulps |
ضرب، دقت یکگانه |
mulsd |
mulpd |
ضرب، دقت مضاعف |
دستورهای SIMD محاسباتی از قاعدهی بالا پیروی میکنند. چند مثال:
addps xmm0, xmm1 ; xmm0 = xmm0 + xmm1 (each 32-bit float at the same position)
subpd xmm2, xmm3 ; xmm2 = xmm2 - xmm3 (each 64-bit float at the same position)
mulps xmm4, xmm5 ; xmm4 = xmm4 * xmm5 (each 32-bit float at the same position)
divpd xmm6, xmm7 ; xmm6 = xmm6 / xmm7 (each 64-bit float at the same position)
دستورهای SIMD دیگری هم برای تبدیل بین مقادیر اعشاری بستهبندیشدهی دقت یکگانه و بستهبندیشدهی دقت مضاعف وجود دارد.
آنها از همان قاعدهی نامگذاری پیروی میکنند، با جایگزین کردن هر s (برای اسکالر) با یک p (برای بستهبندیشده):
| اسکالر | بستهبندیشده | معنی |
|---|---|---|
cvtss2sd |
cvtps2pd |
تبدیل دقت یکگانه به دقت مضاعف |
cvtsd2ss |
cvtpd2ps |
تبدیل دقت مضاعف به دقت یکگانه |
این دستورها نکتهی پیشین را نشان میدهند که خط یک تفسیر است، نه یک محدودیت مطلق. عملوندهای مبدأ و مقصد اندازهی خط متفاوتی دارند، بنابراین تعداد خط آنها هم متفاوت است.
برای cvtps2pd، مبدأ (ps) دارای ۴ خط دقت یکگانه است.
مقصد (pd) دارای ۲ خط دقت مضاعف است.
فقط ۲ خط پایینی مبدأ خوانده میشوند.
۲ خط بالایی نادیده گرفته میشوند:
| عملوند | خط چهارم | خط سوم | خط دوم | خط اول |
|---|---|---|---|---|
مبدأ بهصورت ps
|
نادیده گرفته شده | نادیده گرفته شده | ۲٫۵ | ۱٫۵ |
مقصد بهصورت pd
|
۲٫۵ | ۱٫۵ |
برای cvtpd2ps، مبدأ (pd) دارای ۲ خط دقت مضاعف است.
مقصد (ps) دارای ۴ خط دقت یکگانه است.
هر دو خط مبدأ تبدیل شده و در ۲ خط پایینی مقصد نوشته میشوند.
۲ خط بالایی مقصد صفر میشوند:
| عملوند | خط چهارم | خط سوم | خط دوم | خط اول |
|---|---|---|---|---|
مبدأ بهصورت pd
|
۴٫۰ | ۳٫۰ | ||
مقصد بهصورت ps
|
۰ | ۰ | ۴٫۰ | ۳٫۰ |
چند مثال:
cvtps2pd xmm0, xmm1 ; xmm0 = xmm1 as 2 doubles (low 2 lanes of xmm1 are read)
cvtpd2ps xmm0, xmm1 ; xmm0 = xmm1 as 2 floats (in low 2 lanes; upper 2 zeroed)
دستورهای SIMD بالا همه با عملوندهای ثباتبهثبات نشان داده شدهاند. آنها میتوانند یک عملوند حافظه را هم بهعنوان مبدأ بگیرند (اما نه بهعنوان مقصد).
تعداد بایتهایی که از حافظه خوانده میشود، توسط دستور تعیین میگردد.
برای مثال، addps ۱۶ بایت (۴ عدد اعشاری دقت یکگانه) میخواند.
در مقابل، cvtps2pd فقط ۸ بایت (۲ عدد اعشاری دقت یکگانه) میخواند، چون تمام ورودی مورد نیازش همین است.
addps xmm0, [rsi] ; xmm0 = xmm0 + 4 floats at [rsi]
mulpd xmm1, [rsi + 16] ; xmm1 = xmm1 * 2 doubles at [rsi + 16]
cvtps2pd xmm2, [rsi + 8] ; xmm2 = (2 floats at [rsi + 8]) converted to 2 doubles
وقتی عملوند حافظه ۱۶ بایت عرض دارد، باید ۱۶ بایتی همتراز باشد. یک مکان حافظهی همتراز مکانی است که آدرسش مضربی از اندازهی عملیات باشد (در اینجا ۱۶ بایت).
برای اعلام حافظهی همتراز، میتوانید از دایرکتیو align در بخش .data و .rodata، یا alignb در بخش .bss استفاده کنید:
section .rodata
align 16
mem1: dd 4.0, 3.5, 2.0, -1.0
section .data
align 16
mem2: dq 6.0, 2.1
section .bss
alignb 16
mem3: resb 16
section .text
fn:
movaps xmm0, oword [rel mem1] ; loads 4 packed 32-bit floats in xmm0
movapd xmm1, oword [rel mem2] ; loads 2 packed 64-bit floats in xmm1
movaps oword [rel mem3], xmm0 ; stores 4 packed 32-bit floats in mem3
ret
عملوندهای حافظهی کوچکتر از ۱۶ بایت، مانند مبدأ ۸ بایتی cvtps2pd، هیچ الزام همترازی ندارند.
چهار دستور وجود دارد که ۱۲۸ بیت از مقادیر اعشاری بستهبندیشده را کپی میکنند.
آنها بین دو ثبات xmm، یا بین یک ثبات xmm و یک مکان حافظه کار میکنند:
| دستور | توضیح |
|---|---|
movaps |
دادهی بستهبندیشدهی دقت یکگانه را از یک مکان حافظهی همتراز یا به آن کپی میکند |
movups |
دادهی بستهبندیشدهی دقت یکگانه را از یک مکان حافظهی ناهمتراز یا به آن کپی میکند |
movapd |
دادهی بستهبندیشدهی دقت مضاعف را از یک مکان حافظهی همتراز یا به آن کپی میکند |
movupd |
دادهی بستهبندیشدهی دقت مضاعف را از یک مکان حافظهی ناهمتراز یا به آن کپی میکند |
همهی این دستورها میتوانند برای کپی مقادیر بین دو ثبات xmm استفاده شوند.
اگر عملیات شامل یک مکان حافظه باشد، movaps و movapd لازم میدانند که آن مکان همتراز باشد.
اگر آدرس ناهمترازی داده شود، movaps و movapd یک خطا ایجاد میکنند.
آنها همزمان بهعنوان یک بررسی ایمنی عمل میکنند که چیدمان دادههایتان همان چیزی است که فکر میکنید.
از سوی دیگر، movups و movupd میتوانند با هر مکانی در حافظه استفاده شوند، همتراز یا ناهمتراز.
پیشوند اندازه برای یک مقدار ۱۶ بایتی در NASM، oword است.
SIMD دههها هدف توسعهی فعال در اکوسیستم x86-64 بوده است. هر افزونه به معماری، دستورهای جدید یا کاربردهای تازه برای دستورهای قدیمی به همراه میآورد.
بهویژه، افزونههای AVX و AVX2 تغییرات مرتبطی را معرفی میکنند.
نخست، پشتیبانی از ثباتهای SIMD ۲۵۶ بیتی، به نام ymm.
آنها ثباتهای xmm متناظر را گسترش میدهند، همانطور که rax شامل eax است.
دوم، آنها پشتیبانی از انواع سهعملوندی را برای بیشتر دستورهایی که روی ثباتهای xmm (و ymm) کار میکنند، در بر میگیرند.
این شامل نه فقط دستورهای SIMD، بلکه دستورهایی هم میشود که روی مقادیر اعشاری اسکالر کار میکنند.
این انواع از همان نشانهگذاری معادل SSE خود پیروی میکنند، با یک v که پیش از آنها اضافه میشود:
vaddsd xmm0, xmm1, xmm2 ; xmm0 = xmm1 + xmm2 (scalar sum of 64-bit floats)
vmulps xmm1, xmm3, xmm4 ; xmm1 = xmm3 * xmm4 (packed multiplication of 32-bit floats)
vmovupd ymm5, yword [mem] ; this copies 32 bytes from the location at mem to ymm5
سوم، الزام همترازی برای عملوندهای حافظهی SIMD ملایمتر شده است.
فقط جابهجاییهای صریحاً همتراز، مانند vmovaps، به این همترازی نیاز دارند.
در این ترک، خودمان را به دستورها و ثباتهای تا افزونههای پیشین SSE محدود کردهایم. این برای آموزش مبانی کافی است و سازگاری با طیف گستردهای از پردازندهها را تضمین میکند.
با این حال، test-runner بهطور کامل از AVX و AVX2، و حتی افزونههای جدیدتری که بخشی از AVX-512 هستند، پشتیبانی میکند. میتوانید آزادانه از آنها استفاده کنید.
کار خوبی است که در استفاده از گونههای SIMD سازگاری داشته باشید. سعی کنید برای دستورهای یکسان، گونههای SSE و AVX را با هم مخلوط نکنید.
اگر پایگاه کد شما این گونهها را مخلوط میکند، کار خوبی است که پس از پایان کارتان با AVX و پیش از استفاده از SSE، از vzeroupper استفاده کنید.
این کار زنجیرههای وابستگی را میشکند و از یک توقف پرهزینه جلوگیری میکند، آنگاه که پردازنده بین حالت AVX و SSE جابهجا میشود.
شما در حال نوشتن کد برای سیستم دادهی سالن یک کارخانهی تولید قطعات خودرو هستید. قطعات موتور و شاسی از میان مجموعهای از ایستگاههای ماشینکاری عبور میکنند. هر ایستگاه اندازهگیریهایی تولید میکند که پیش از اجرای مرحلهی بعدی باید با هم ترکیب، مقایسه، کالیبره یا نرمالسازی شوند. توابعی که در اینجا پیادهسازی میکنید، یکی برای هر ایستگاه است.
محاسبات این تمرین باید با استفاده از دستورهای SIMD انجام شوند، نه با عملیات اسکالر.
در سالن کارخانه، دو خط مونتاژ موازی کار میکنند. در پایان هر شیفت، هر دو خط بازدهی خود را برای ۴ سبد قطعه بهصورت اعداد دقت ساده گزارش میکنند. داشبورد سرپرست، بازدهی ترکیبی هر سبد را نشان میدهد، بنابراین دو گزارش، سبد به سبد با هم جمع میشوند. هر سه بافر توسط سیستم گزارشدهی مدیریت میشوند و همیشه با مرز ۱۶ بایت همتراز هستند.
تابعی به نام sum_yields تعریف کنید که با گرفتن بازدهی هر سبد از line_a و line_b، جمع به تفکیک سبد را در result مینویسد.
آرگومانها، به ترتیب، عبارتاند از:
line_a: نشانی حافظهی همتراز با مرز ۱۶ بایت که ۴ عدد ممیز شناور ۳۲ بیتی را نگه میداردline_b: نشانی حافظهی همتراز با مرز ۱۶ بایت که ۴ عدد ممیز شناور ۳۲ بیتی را نگه میداردresult: نشانی حافظهی همتراز با مرز ۱۶ بایت که تابع ۴ عدد ممیز شناور ۳۲ بیتی را در آن مینویسداین تابع مقدار بازگشتی ندارد؛ در عوض، نتیجه در بافر پاسدادهشده (result) ذخیره میشود:
line_a = {10.0, 20.0, 30.0, 40.0};
line_b = { 5.0, 15.0, 25.0, 35.0};
sum_yields(line_a, line_b, result);
// => result == {15.0, 35.0, 55.0, 75.0};
بین عملیات، یک ایستگاه نمونهبرداری قطعات ماشینکاریشده را اندازه میگیرد و آنها را با ابعاد هدف مقایسه میکند. مقادیر اندازهگیریشده از یک گیج دقیق بهصورت جریانی وارد بافری میشوند که همیشه با مرز ۱۶ بایت همتراز است. مقادیر هدف از بستهی دستور کار میآیند که تضمینی برای همترازی ندارد. انحراف پیش از آنکه به بررسیکنندهی پاییندستی تلورانس پاس داده شود، در یک ضریب حساسیت بهازای هر محور ضرب میشود. در این مرحله، بررسیکنندهی پاییندستی انحراف را بهجای حافظه، مستقیم از یک ثبات SIMD میخواند.
تابعی به نام scaled_deviation تعریف کنید که با گرفتن measured و target و یک sensitivity، انحراف مقیاسشده را در result مینویسد.
آرگومانها، به ترتیب، عبارتاند از:
measured: نشانی حافظهی همتراز با مرز ۱۶ بایت که ۲ عدد ممیز شناور ۶۴ بیتی را نگه میداردtarget: نشانی حافظهای که ۲ عدد ممیز شناور ۶۴ بیتی را نگه میدارد (همترازی تضمینشده نیست)sensitivity: نشانی حافظهای که ۲ عدد ممیز شناور ۶۴ بیتی را نگه میدارد (همترازی تضمینشده نیست)result: نشانی حافظهای برای بافری که تابع ۲ عدد ممیز شناور ۶۴ بیتی را در آن مینویسد (همترازی تضمینشده نیست)انحراف مقیاسشده، بهصورت لاینبهلاین، برابر است با (measured - target) * sensitivity.
این تابع مقدار بازگشتی ندارد؛ در عوض، نتیجه بهصورت یک جفت عدد ممیز شناور ۶۴ بیتی در بافر پاسدادهشده (result) ذخیره میشود.
measured = {10.5, 20.5};
target = {10.0, 20.0};
sensitivity = { 2.0, 4.0};
scaled_deviation(measured, target, sensitivity, result);
// => result == {1.0, 2.0};
پیش از آنکه قطعات وارد جایگاه بازرسی نهایی شوند، خوانشهای ابعادی یک دستهی ۴ قطعهای به مقیاس دقت دوگانه درآورده میشوند و برای دریفت دمایی معلوم تصحیح میشوند.
۴ خوانش خام بهصورت اعداد دقت ساده میرسند.
برای هر سطر از ۲ خوانش، کالیبراسیون reference / (raw - offset) را محاسبه میکند و سپس این نتیجه را در یک ضریب تبدیل ثابت 0.5 ضرب میکند.
توجه کنید که reference و offset برای هر دو سطر دسته یکسان هستند.
ضریب تبدیل در هر کالیبراسیون یکسان است؛ بنابراین بهجای آنکه آن را بهعنوان آرگومان دریافت کنید، باید آن را بهصورت یک ثابت ۱۶ بایتی بستهبندیشده در حافظه تعریف کنید.
تابعی به نام calibrate_batch تعریف کنید که با گرفتن یک دستهی raw، یک reference، یک offset و یک بافر result، مقادیر کالیبرهشده را در result مینویسد.
آرگومانها، به ترتیب، عبارتاند از:
raw: نشانی حافظهای که ۴ عدد ممیز شناور ۳۲ بیتی را نگه میدارد (همترازی تضمینشده نیست)reference: نشانی حافظهی همتراز با مرز ۱۶ بایت که ۲ عدد ممیز شناور ۶۴ بیتی را نگه میداردoffset: نشانی حافظهی همتراز با مرز ۱۶ بایت که ۲ عدد ممیز شناور ۶۴ بیتی را نگه میداردresult: نشانی حافظهی همتراز با مرز ۱۶ بایت که تابع ۴ عدد ممیز شناور ۶۴ بیتی را در آن مینویسد (در مجموع ۳۲ بایت)تابع باید برای هر یک از دو سطر دسته، کارهای زیر را انجام دهد:
offset را از جفت تبدیلشده کم کند.reference را بر نتیجه تقسیم کند.0.5 ضرب کند.result ذخیره کند.این تابع مقدار بازگشتی ندارد؛ در عوض، نتیجه در بافر پاسدادهشده (result) ذخیره میشود:
raw = {10.0, 20.0, 25.0, 50.0};
reference = {100.0, 100.0};
offset = { 5.0, 10.0};
calibrate_batch(raw, reference, offset, result);
// => result == {10.0, 5.0, 2.5, 1.25};
در انتهای خط، هر قطعه یک امتیاز کیفیت دارد که پیش از گزارش شدن باید نرمالسازی شود. هر امتیاز در بهرهی مربوط به قطعهی خودش ضرب میشود و سپس بر مقیاس تولید هر محور تقسیم میشود. بافر بهرهها برای هر قطعه یک بهره در خود دارد.
مقیاس تولید برای کل اجرا یکسان است و در نشانی حافظهای پاس داده میشود که دو عدد ممیز شناور ۶۴ بیتی را در خود دارد. امتیازها درجا بهروزرسانی میشوند: همان بافری که امتیازهای خام را در خود داشت، اکنون امتیازهای نرمالشده را نگه میدارد.
تابعی به نام normalize_scores تعریف کنید که با گرفتن یک آرایهی scores، یک آرایهی gains، یک scale و یک شمارش n، امتیازها را درجا نرمالسازی میکند.
آرگومانها، به ترتیب، عبارتاند از:
scores: نشانی حافظهی همتراز با مرز ۱۶ بایت که n عدد ممیز شناور ۶۴ بیتی را نگه میدارد (درجا تغییر میکند)gains: نشانی حافظهی همتراز با مرز ۱۶ بایت که n عدد ممیز شناور ۶۴ بیتی را نگه میداردscale: نشانی حافظهی همتراز با مرز ۱۶ بایت که ۲ عدد ممیز شناور ۶۴ بیتی را نگه میداردn: یک عدد صحیح مثبت و زوج ۶۴ بیتی (حداقل 2) که تعداد امتیازهایی است که باید نرمالسازی شونددو لاین مقیاس به دو عنصر هر جفت اعمال میشوند.
لاین اول، عنصر اول هر جفت را مقیاس میکند (لاینهای در موقعیتهای زوج) و لاین دوم، عنصر دوم را (لاینهای در موقعیتهای فرد).
هر امتیاز در بهرهی متناظر خود ضرب میشود و سپس بر لاین مقیاس مناسب تقسیم میشود: score * gain / scale_lane.
این تابع مقدار بازگشتی ندارد؛ در عوض، نتیجه در بافر ورودی (scores)، در همان موقعیت score متناظری که نرمالسازی شده بود، ذخیره میشود:
n = 4
scores = {10.0, 20.0, 30.0, 40.0};
gains = { 2.0, 1.5, 1.0, 0.5};
scale = { 2.0, 4.0};
normalize_scores(scores, gains, scale, n);
// => scores == {10.0, 7.5, 15.0, 5.0};
در Exercism ثبتنام کنید تا x86-64 Assembly را همراه با 22 مفهوم130 تمرین و مربیگری انسانی واقعی یاد بگیرید و در آن استاد شوید، همهی اینها رایگان.