کد اسکالر برای انشعاب بر اساس یک شرط خاص، به پرچمهایی تکیه میکند که دستورهای مختلف تنظیم میکنند. اما مقادیر بستهبندیشده نه یک مقدار، بلکه چندین مقدار را بهصورت موازی نمایش میدهند. ممکن است یک شرط برای یک خط برقرار نباشد و برای خط دیگری برقرار باشد.
به همین دلیل است که کد SIMD بهطور پیشفرض بدون انشعاب است.
مقایسههای بستهبندیشده، بهجای تکیه بر پرچمها، معمولاً یک «ماسک» را در عملوند مقصد تولید میکنند.
این مقایسه برای هر خط، در صورت درست بودن کل خط را با یک و در صورت نادرست بودن با صفر پر میکند.
اگر بهعنوان یک عدد صحیح علامتدار خوانده شود، خط درست -1 و خط نادرست 0 است.
سپس میتوان این ماسک را با عملیات بیتی ترکیب کرد تا خطهای خاصی را فیلتر کرد.
یک cmp اسکالر از این نظر عمومی است که برای تنظیم چند پرچم بهطور همزمان به کار میرود.
سپس یک دستور دیگر میتواند از آن پرچمها برای انشعاب یا انجام محاسبات استفاده کند.
اما چون یک مقایسهی بستهبندیشده همزمان هم یک شرط را بررسی میکند و هم یک ماسک را محاسبه میکند، عمومی نیست. باید شرط دقیقی که آزموده میشود به مقایسه داده شود.
برای این کار دو راه وجود دارد:
eq برای برابری و gt برای بزرگتری.
سایر گونهها با ترکیب نتیجهی یکی از این دو ساخته میشوند.جز استفاده از یک پسوند شرطی خاص در مقایسههای صحیح، نحوهی نگارش از همان ساختاری پیروی میکند که پیشتر دیدهایم:
p + cmp + شرط + اندازه (b، w، d یا q).cmp + p + اندازه (s یا d).
شرط بهصورت یک عملوند اضافه در یک مقدار فوری پاس داده میشود.همانطور که اشاره شد، مقایسههای صحیح فقط برای برابری و بزرگتری وجود دارند:
| دستور | توضیح |
|---|---|
pcmpeqb, pcmpeqw, pcmpeqd, pcmpeqq
|
برابری در هر خط |
pcmpgtb, pcmpgtw, pcmpgtd, pcmpgtq
|
بزرگتری علامتدار در هر خط |
movdqa xmm0, [rel scores]
pcmpgtd xmm0, [rel threshold] ; lane i = 0xFFFFFFFF (-1) if scores[i] > threshold[i], else 0
برای ساختن یک مقایسهی کوچکتری، از gt با جابهجایی عملوندها استفاده کنید: a < b == b > a.
توجه کنید که این مقایسه علامتدار است. برای انجام یک مقایسهی بدون علامت، بیت بالای هر دو عملوند را وارونه کنید. این کار با یک XOR با ماسکی انجام میشود که تنها بیت بالای آن تنظیم شده است.
دو ترفند کاربردی وجود دارد:
برای نمونه:
pxor xmm4, xmm4 ; xmm4 = all zeros
pcmpeqd xmm7, xmm7 ; xmm7 = all ones
ماسک همهصفر و ماسک همهیک، ماسکهای رایجی هستند که به ترتیب به معنای «غلط در همهجا» و «درست در همهجا» هستند.
از آنها همچنین میتوان برای نمایش 0 یا -1 بستهبندیشده استفاده کرد که مقادیر نشانهی رایجی هستند.
برای نمونه، NUL که پایان یک رشته را مشخص میکند، یک 0 است.
خطهای ممیز شناور شکل متفاوتی دارند: یک دستور، cmpps (و cmppd برای خطهای ۶۴بیتی)، با شرط بهصورت یک مقدار فوری:
movaps xmm0, [rel readings]
cmpps xmm0, [rel limits], 1 ; condition 1 is "less than": lane i = all ones if readings[i] < limits[i]
NASM شبهدستورهایی هم دارد که به مقدار فوری درست نگاشته میشوند و بهخاطر سپردنشان آسانتر است.
در همهی موارد زیر، x در px میتواند s (اعداد شناور ۳۲بیتی) یا d (اعداد شناور ۶۴بیتی) باشد:
| شبهدستور | مقدار فوری | مقایسه |
|---|---|---|
cmpeqpx |
0 | a == b |
cmpltpx |
1 | a < b |
cmplepx |
2 | a <= b |
cmpunordpx |
3 | a برابر NaN است یا b برابر NaN است |
cmpneqpx |
4 | a != b |
cmpnltpx |
5 | a >= b |
cmpnlepx |
6 | a > b |
cmpordpx |
7 | هیچکدام از a و b برابر NaN نیست |
یک ماسک نتیجهی یک شرط را رمزگذاری میکند. سپس میتوان از آن برای انتخاب خطبهخط بین دو مجموعه مقدار بر اساس آن شرط استفاده کرد. خط را در جایی که ماسک درست است از یک مقدار و در جایی که نادرست است از مقدار دیگری برمیداریم:
; result = (a AND mask) OR (b AND NOT mask)
movdqa xmm2, xmm0 ; xmm0 holds the mask, keep a copy
pand xmm2, xmm3 ; xmm2 = a AND mask: lanes of a where mask is true
pandn xmm0, xmm4 ; xmm0 = NOT mask AND b: lanes of b where mask is false
por xmm2, xmm0 ; combine the two halves
توجه کنید که نامتقارن بودن pandn اینجا به کار میآید: ماسک در مقصد قرار میگیرد، نقیض میشود و در یک دستور واحد از b انتخاب میکند.
این الگو شکل بستهبندیشدهی انتخاب بدون انشعاب است.
هر خط محاسبه میشود و تنها ماسک تعیین میکند که کدام مقدار باقی میماند، بدون هیچ jcc در هیچجا.
دستورهایی وجود دارند که همان انتخاب را بهطور مستقیم انجام میدهند و برای هر عنصر یک بیت از یک ثبات ماسک میخوانند. به اینها دستورهای «درهمآمیزی» میگویند:
| دستور | عنصر | منبع ماسک |
|---|---|---|
pblendvb |
بایت |
xmm0 ضمنی |
blendvps |
خط ۳۲بیتی |
xmm0 ضمنی |
blendvpd |
خط ۶۴بیتی |
xmm0 ضمنی |
توجه کنید که دستور اول از نحوهی نگارش اعداد صحیح پیروی میکند، در حالی که دو دستور دیگر از نحوهی نگارش اعداد شناور پیروی میکنند. اما چون این دستورها صرفاً بایتهای خام را انتخاب میکنند، هر کدام از آنها را میتوان هم با اعداد صحیح و هم با اعداد شناور به کار برد.
درهمآمیزی برای هر عنصر، وقتی بیت بالای عنصر متناظر در ماسک پاک باشد مقصد را نگه میدارد و وقتی تنظیم شده باشد منبع را برمیدارد.
فقط همان بیت بالا بررسی میشود، که یک ماسک مقایسه آن را برآورده میکند، چون خطهایش همه یک یا همه صفر هستند.
ثبات ماسک همیشه xmm0 است که بهصورت ضمنی در نظر گرفته میشود:
movaps xmm0, [rel mask] ; the selecting mask must be in xmm0
movaps xmm1, [rel b] ; destination: kept where the mask bit is clear
blendvps xmm1, [rel a] ; source: taken where the mask bit is set
همچنین میتوان از pblendvb برای انتخاب خطها از یک ماسک مقایسه برای هر اندازهی دیگری استفاده کرد.
چون همهی بایتهای یک خط درست همگی یک هستند، pblendvb همهی آنها را انتخاب میکند.
این دستورها همگی یک v بعد از عملیاتی که انجام میدهند (blend) اضافه میکنند.
این v مخفف variable است، چون انتخاب ایستا نیست: به یک ثبات وابسته است.
گونههایی هم بدون v وجود دارند که بر اساس یک مقدار فوری انتخاب میکنند.
اینها از همان الگو پیروی میکنند و خط i را در صورتی انتخاب میکنند که بیت i در مقدار فوری تنظیم شده باشد.
کد SIMD با وجود قدرتمند بودن، بسیاری از انعطافپذیری کد اسکالر را ندارد. در بسیاری از موقعیتها لازم است از یک ثبات بستهبندیشده به دنیای دستورهای اسکالر بازگردیم.
خانوادهی دستورهای movmsk بهعنوان پلی میان این دو دنیا عمل میکند.
این دستورها بیت بالای هر خط را در یک ثبات همهکاره استخراج میکنند:
| دستور | جمعآوری میکند | عرض نتیجه |
|---|---|---|
pmovmskb |
بیت بالای هر یک از ۱۶ بایت | ۱۶ بیت |
movmskps |
بیت بالای هر یک از ۴ dword | ۴ بیت |
movmskpd |
بیت بالای هر یک از ۲ qword | ۲ بیت |
اگر بعد از یک مقایسه استفاده شود، هر بیت تنظیمشده نمایندهی یک خط «درست» و هر بیت پاکشده نمایندهی یک خط «غلط» است.
سپس میتوان این نتیجه را مانند همیشه با دستورهای اسکالر دستکاری کرد.
برای نمونه، popcnt تعداد تطابقها را میشمارد و tzcnt اولین بیت را پیدا میکند.
ثبات همهکاره میتواند ۳۲ یا ۶۴ بیت عرض داشته باشد.
یک گونهی بستهبندیشده از دستور اسکالر test هم وجود دارد: ptest.
این دستور شبیه همتای اسکالر خود است، از این نظر که یک عملیات AND بین دو عملوند انجام میدهد بدون آنکه آنها را تغییر دهد.
برخلاف test، دستور ptest یک عملیات ANDN هم انجام میدهد که عملوند اول را نقیض میکند.
بنابراین میتوان ptest را نسخهی غیرمخرب pand و pandn در نظر گرفت که پرچمها را بر اساس نتیجه تنظیم میکند.
ptest تقریباً مانند همان دو دستور، کل ثبات SIMD را همچون یک خط واحد در نظر میگیرد و بنابراین پیشوند اندازه نمیگیرد.
اگر نتیجهی یک عملیات AND برابر 0 باشد، پرچم ZF تنظیم میشود و اگر نتیجهی عملیات ANDN برابر 0 شود، این پرچم CF است که تنظیم میشود.
این یعنی میتوان از ptest برای بررسی هم ماسک همهیک و هم ماسک همهصفر استفاده کرد:
ptest روی یک ثبات با خودش، پرچم ZF را فقط در صورتی تنظیم میکند که ثبات تمامصفر باشد.
این کار همان ترفند رایج اسکالر را تقلید میکند که در آن test روی یک ثبات با خودش برای بررسی 0 به کار میرود.ptest روی یک ثبات با یک ماسک همهیک، پرچم CF را فقط در صورتی تنظیم میکند که ثبات همهیک باشد.
همچنین پرچم ZF را فقط در صورتی تنظیم میکند که ثبات همهصفر باشد و این امکان را میدهد که هر دو ماسک را همزمان بررسی کنیم.pxor xmm0, xmm0 ; all zeros
pcmpeqb xmm1, xmm1 ; all ones
pcmpeqb xmm2, xmm2
ptest xmm0, xmm0 ; ZF set: a register against itself detects all zeros
ptest xmm0, xmm1 ; ZF set, CF clear: xmm0 is all zeros, not all ones
ptest xmm2, xmm1 ; CF is set only if xmm2 is all ones
از نتیجهی یک ptest میتوان مانند همیشه برای انشعاب یا در دستورهای بدون انشعاب مانند setcc یا cmovcc استفاده کرد.
شما ایستگاه تصحیح یک مدرسه را اداره میکنید و نتایج کلاس را بلوکبهبلوک نمرهدهی میکنید.
هر بلوک ۴ نتیجه را در خود جای میدهد و ایستگاه همان عملیات را روی هر نتیجهی داخل بلوک اعمال میکند. نمره یک عدد ممیز شناور ۳۲ بیتی است. چند مرحله با یک «ماسک» کار میکنند: بلوکی از ۴ خط که هر خط یا کاملاً یک است (یعنی «بله» برای آن نتیجه) یا کاملاً صفر (یعنی «خیر»).
شما پنج کار دارید. عملوندها را از طریق آدرسهای حافظه دریافت میکنید. بعضی کارها پاسخشان را در یک آدرس نتیجه مینویسند و بعضی دیگر آن را مستقیم برمیگردانند.
همهی آدرسهای حافظه در این تمرین روی مرز ۱۶ بایت تراز شدهاند.
محاسبات این تمرین باید با دستورهای SIMD انجام شوند.
مرحلهی اول هر نتیجه را در برابر یک آستانه نمرهدهی میکند. یک نتیجه زمانی از آستانه میگذرد که نمرهاش اکیداً بزرگتر از آستانه باشد. هر نمرهای که کوچکتر یا مساوی آستانه باشد، از آن نمیگذرد.
تابع flag_above_threshold را پیادهسازی کنید؛ این تابع ماسکی میسازد که برای هر نمرهی بالای آستانهاش خطی همگی یک و در غیر این صورت خطی همگی صفر دارد.
این تابع این آرگومانها را به این ترتیب دریافت میکند:
result: آدرس حافظهای برای بافری که ۴ خط ماسک در آن نوشته میشود.scores: آدرس حافظهی نمرهها، شامل ۴ عدد ممیز شناور ۳۲ بیتی معمولی (هرگز NaN).thresholds: آدرس حافظهی آستانهی هر خط، شامل ۴ عدد ممیز شناور ۳۲ بیتی معمولی (هرگز NaN).scores = {72.0, 55.0, 90.0, 40.0}
thresholds = {60.0, 60.0, 60.0, 60.0}
result = {0xFFFFFFFF, 0x00000000, 0xFFFFFFFF, 0x00000000}
این تابع مقدار بازگشتی ندارد.
گزارشی جداگانه نتایج کامل را برجسته میکند؛ همانهایی که به بالاترین نمرهی ممکن رسیدهاند.
تابع flag_perfect را پیادهسازی کنید؛ این تابع ماسکی میسازد که برای هر نمرهای که با بیشینهاش برابر است خطی همگی یک و در غیر این صورت خطی همگی صفر دارد.
این تابع این آرگومانها را به این ترتیب دریافت میکند:
result: آدرس حافظهای برای بافری که ۴ خط ماسک در آن نوشته میشود.scores: آدرس حافظهی نمرهها، شامل ۴ عدد ممیز شناور ۳۲ بیتی معمولی (هرگز NaN).maxima: آدرس حافظهی بیشینهی هر خط، شامل ۴ عدد ممیز شناور ۳۲ بیتی معمولی (هرگز NaN).scores = {100.0, 88.0, 100.0, 73.0}
maxima = {100.0, 100.0, 100.0, 100.0}
result = {0xFFFFFFFF, 0x00000000, 0xFFFFFFFF, 0x00000000}
این تابع مقدار بازگشتی ندارد.
هر نمره رتبهای از ۱ تا ۳ میگیرد:
50.0.تابع assign_ranks را پیادهسازی کنید؛ این تابع رتبهی هر نمره را مینویسد.
بهتر است آستانهی قبولی و مقادیر رتبه را بهصورت ثابتهای بستهبندیشده در حافظه تعریف کنید. میتوانید از توابع دو کار قبلی دوباره استفاده کنید: نمره وقتی بالای آستانه باشد حداقل رتبهی ۲ دارد و وقتی با بیشینه برابر باشد رتبهی ۳.
این تابع این آرگومانها را به این ترتیب دریافت میکند:
result: آدرس حافظهای برای بافری که ۴ رتبه در آن نوشته میشود، هر کدام یک عدد صحیح بدون علامت ۳۲ بیتی.scores: آدرس حافظهی نمرهها، شامل ۴ عدد ممیز شناور ۳۲ بیتی معمولی (هرگز NaN).maxima: آدرس حافظهی بیشینهی هر خط، شامل ۴ عدد ممیز شناور ۳۲ بیتی معمولی (هرگز NaN).scores = {40.0, 75.0, 100.0, 60.0}
maxima = {100.0, 100.0, 100.0, 100.0}
result = {1, 2, 3, 2}
این تابع مقدار بازگشتی ندارد.
در طول سال، هر دانشآموز یک رتبهی کلی برای خود جمع میکند. ایستگاه میشمارد که چند رتبه در کل گروه زیر آستانهی قبولی میافتند تا برنامهریزی کند چند کلاس اضافه برگزار شود.
تابع count_failures را پیادهسازی کنید؛ این تابع برمیگرداند که چند رتبه در همهی بلوکها اکیداً زیر آستانهی قبولی قرار دارند.
آستانه بهصورت بلوکی از ۴ خط یکسان داده میشود، پس میتوانید آن را یک بار بارگذاری کنید و برای هر بلوک دوباره به کار ببرید.
این تابع این آرگومانها را به این ترتیب دریافت میکند:
ranks: آدرس حافظهی رتبهها؛ به تعداد صحیحی از بلوکهای ۴ خطی، که هر رتبه یک عدد صحیح بدون علامت ۳۲ بیتی است.block_count: تعداد بلوکهای ۴ خطی که همیشه بزرگتر از 0 است.pass_threshold: آدرس حافظهی آستانهی قبولی، شامل ۴ عدد صحیح ۳۲ بیتی یکسان.ranks = {1, 2, 3, 1, 2, 2, 1, 3} // 2 blocks
block_count = 2
pass_threshold = {2, 2, 2, 2}
// => 3
این تابع تعداد را بهصورت یک عدد صحیح علامتدار ۳۲ بیتی برمیگرداند.
پیش از آنکه سوابق بایگانی شوند، ایستگاه بررسی میکند که گروه پاک باشد: زمانی قبول میشود که هیچ نتیجهای در هیچ بلوکی مردود نشده باشد.
تابع all_passed را پیادهسازی کنید؛ این تابع اگر همهی دانشآموزان قبول شده باشند 1 و در غیر این صورت 0 برمیگرداند.
یک دانشآموز زمانی قبول است که خط متناظرش در آرایهی failing همگی صفر باشد.
این تابع این آرگومانها را به این ترتیب دریافت میکند:
failing: آدرس حافظهی ماسکهای مردودی؛ به تعداد صحیحی از بلوکهای ۴ خطی، که هر خط آن همگی یک یا همگی صفر است.block_count: تعداد بلوکهای ۴ خطی که همیشه بزرگتر از 0 است.failing = {0x00000000, 0x00000000, 0x00000000, 0x00000000,
0x00000000, 0x00000000, 0x00000000, 0x00000000} // 2 blocks
block_count = 2
// => 1
این تابع پاسخ را بهصورت یک عدد صحیح علامتدار ۳۲ بیتی، یعنی 1 یا 0، برمیگرداند.
در Exercism ثبتنام کنید تا x86-64 Assembly را همراه با 22 مفهوم130 تمرین و مربیگری انسانی واقعی یاد بگیرید و در آن استاد شوید، همهی اینها رایگان.