کارنامه

کارنامه

تمرین یادگیری

مقدمه

SIMD: ماسک و شرط

کد اسکالر برای انشعاب بر اساس یک شرط خاص، به پرچم‌هایی تکیه می‌کند که دستورهای مختلف تنظیم می‌کنند. اما مقادیر بسته‌بندی‌شده نه یک مقدار، بلکه چندین مقدار را به‌صورت موازی نمایش می‌دهند. ممکن است یک شرط برای یک خط برقرار نباشد و برای خط دیگری برقرار باشد.

به همین دلیل است که کد 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 با ماسکی انجام می‌شود که تنها بیت بالای آن تنظیم شده است.

Note

دو ترفند کاربردی وجود دارد:

  1. XOR کردن یک ثبات با خودش تا همه‌ی بیت‌ها صفر شوند.
  2. مقایسه‌ی یک ثبات با خودش تا همه‌ی بیت‌ها یک شوند.

برای نمونه:

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 همه‌ی آن‌ها را انتخاب می‌کند.

Note

این دستورها همگی یک 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 برای بررسی هم ماسک همه‌یک و هم ماسک همه‌صفر استفاده کرد:

  1. استفاده از ptest روی یک ثبات با خودش، پرچم ZF را فقط در صورتی تنظیم می‌کند که ثبات تمام‌صفر باشد. این کار همان ترفند رایج اسکالر را تقلید می‌کند که در آن test روی یک ثبات با خودش برای بررسی 0 به کار می‌رود.
  2. استفاده از 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 استفاده کرد.

دستورالعمل‌ها

شما ایستگاه تصحیح یک مدرسه را اداره می‌کنید و نتایج کلاس را بلوک‌به‌بلوک نمره‌دهی می‌کنید.

هر بلوک ۴ نتیجه را در خود جای می‌دهد و ایستگاه همان عملیات را روی هر نتیجه‌ی داخل بلوک اعمال می‌کند. نمره یک عدد ممیز شناور ۳۲ بیتی است. چند مرحله با یک «ماسک» کار می‌کنند: بلوکی از ۴ خط که هر خط یا کاملاً یک است (یعنی «بله» برای آن نتیجه) یا کاملاً صفر (یعنی «خیر»).

شما پنج کار دارید. عملوندها را از طریق آدرس‌های حافظه دریافت می‌کنید. بعضی کارها پاسخشان را در یک آدرس نتیجه می‌نویسند و بعضی دیگر آن را مستقیم برمی‌گردانند.

همه‌ی آدرس‌های حافظه در این تمرین روی مرز ۱۶ بایت تراز شده‌اند.

Note

محاسبات این تمرین باید با دستورهای SIMD انجام شوند.

1. نمره‌های بالای آستانه را علامت بزنید

مرحله‌ی اول هر نتیجه را در برابر یک آستانه نمره‌دهی می‌کند. یک نتیجه زمانی از آستانه می‌گذرد که نمره‌اش اکیداً بزرگ‌تر از آستانه باشد. هر نمره‌ای که کوچک‌تر یا مساوی آستانه باشد، از آن نمی‌گذرد.

تابع 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}

این تابع مقدار بازگشتی ندارد.

2. نمره‌های کامل را علامت بزنید

گزارشی جداگانه نتایج کامل را برجسته می‌کند؛ همان‌هایی که به بالاترین نمره‌ی ممکن رسیده‌اند.

تابع 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}

این تابع مقدار بازگشتی ندارد.

3. رتبه اختصاص دهید

هر نمره رتبه‌ای از ۱ تا ۳ می‌گیرد:

  • رتبه‌ی ۱ برای نمره‌ای برابر یا کمتر از آستانه‌ی قبولی 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}

این تابع مقدار بازگشتی ندارد.

4. مردودی‌ها را بشمارید

در طول سال، هر دانش‌آموز یک رتبه‌ی کلی برای خود جمع می‌کند. ایستگاه می‌شمارد که چند رتبه در کل گروه زیر آستانه‌ی قبولی می‌افتند تا برنامه‌ریزی کند چند کلاس اضافه برگزار شود.

تابع 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

این تابع تعداد را به‌صورت یک عدد صحیح علامت‌دار ۳۲ بیتی برمی‌گرداند.

5. آیا همه قبول شدند؟

پیش از آنکه سوابق بایگانی شوند، ایستگاه بررسی می‌کند که گروه پاک باشد: زمانی قبول می‌شود که هیچ نتیجه‌ای در هیچ بلوکی مردود نشده باشد.

تابع 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، برمی‌گرداند.

ویرایش از طریق GitHub این لینک در پنجره یا زبانه‌ی جدیدی باز می‌شود
x86-64 Assembly Exercism

آماده‌اید کارنامه را شروع کنید؟

در Exercism ثبت‌نام کنید تا x86-64 Assembly را همراه با 22 مفهوم130 تمرین و مربی‌گری انسانی واقعی یاد بگیرید و در آن استاد شوید، همه‌ی این‌ها رایگان.