تبديل القنوات

تبديل القنوات

تمرين تعلّمي

مقدمة

SIMD: العمليات العابرة للمسارات

كل عملية SIMD رأيناها حتى الآن كانت تعمل مسارًا بمسار. تُحسب القيمة في المسار i من النتيجة اعتمادًا على المسار i من المدخلات.

ومع ذلك، هناك حالات كثيرة يكون فيها نقل القيم بين المسارات ضروريًا أو مرغوبًا. على سبيل المثال، قد تكون المسارات بترتيب غير مناسب للعملية الحسابية التي نريد تنفيذها.

هناك تعليمات SIMD كثيرة تنقل البيانات بين المسارات بطرق مختلفة.

الخلط

الخلط يعيد ترتيب البايتات أو المسارات في سجل، بحيث يأخذ كل مسار في الوجهة قيمته من مسار في المصدر قد يكون في أي موضع. ويمكنه اختيار المسار نفسه لعدة وجهات، ما يجعله قادرًا أيضًا على بث القيم.

تتصرف تعليمات الخلط بطرق مختلفة حسب حجمها ونطاق تنفيذها.

اختيار المسارات بقيمة فورية

تعيد التعليمة pshufd ترتيب المسارات الأربعة ذات 32 بت من المصدر إلى الوجهة.

الاختيار عبارة عن قيمة فورية من 8 بتات، تُقرأ كأربعة حقول من بتَّين لكل حقل، حقل واحد لكل مسار في الوجهة. يحدد كل حقل أيًّا من مسارات المصدر الأربعة يُنسخ إلى ذلك المسار في الوجهة:

الحقل فهرس المسار
00 0
01 1
10 2
11 3

يشير موضع الحقل داخل القيمة الفورية، عند قراءتها من اليمين إلى اليسار، إلى المكان الذي يُدرج فيه المسار المختار. يمكن اختيار مسار مصدر أكثر من مرة، وهكذا يُبث مسار واحد إلى السجل كله:

; The bit fields are read right-to-left
pshufd xmm1, xmm0, 0b00_01_10_11 ; reverse: lane 0 takes source 0b11 (3), lane 1 takes source 0b10 (2), and so on
pshufd xmm3, xmm2, 0b00_00_00_00 ; broadcast source lane 0 into all four lanes
خلط مسارات الأعداد ذات الفاصلة العائمة

هناك تعليمتا خلط للأعداد ذات الفاصلة العائمة، تتبعان الصيغة العامة نفسها: shuf + p + لاحقة الحجم (إما s أو d).

وهما تستخدمان أيضًا قيمة فورية لاختيار المسارات. تستخدم shufps الترميز نفسه بحقول البتَّين الذي تستخدمه تعليمات الخلط الصحيحة السابقة. لكن بما أن المعامل ذا 128 بت يحتوي على مسارين فقط من 64 بت، فإن shufpd تستخدم ترميز حقل من بتة واحدة فقط.

تختلف هاتان التعليمتان عن نظيرتيهما من الأعداد الصحيحة في أنهما تأخذان المسارات من معاملين لا من معامل واحد:

  • يُؤخذ النصف الأدنى من النتيجة من معامل الوجهة.
  • ويُؤخذ النصف الأعلى من معامل المصدر.
shufps xmm0, xmm1, 0b11_10_01_00 ; xmm0 = {xmm0[0], xmm0[1], xmm1[2], xmm1[3]}
shufps xmm2, xmm3, 0b00_00_00_00 ; xmm2 = {xmm2[0], xmm2[0], xmm3[0], xmm3[0]}
shufpd xmm4, xmm5, 0b0_1         ; xmm4 = {xmm4[1], xmm5[0]}

ولأن المعاملين معًا يغذيان النتيجة، يمكن استخدامهما لتداخل متجهين في خطوة واحدة. وإذا كان المصدر والوجهة السجل نفسه، فسيؤخذ كل مسار منه.

خلط البايتات

pshufb هي عملية الخلط الأكثر عمومية. فرغم أن pshufb و pshufd اسمهما متشابه جدًا ولا يختلفان إلا في لاحقة الحجم، فإنهما تنفذان عمليتين مختلفتين تمامًا.

أولًا، بينما تستخدم pshufd قيمة فورية لاختيار مواضع المسارات، تستخدم pshufb متجه تحكم في معامل المصدر. متجه التحكم هذا إما سجل xmm أو معامل ذاكرة من 16 بايت.

لكل مسار من المسارات الستة عشر في الوجهة، تعطي البتات الأربع الدنيا من متجه التحكم فهرس بايت المصدر، من 0 إلى 15. وإذا كان المسار i من متجه التحكم يحمل فهرس بايت المصدر j، فسيُنقل المسار رقم j-th من الوجهة إلى المسار رقم i.

وهذا يبرز فارقًا ثانيًا بين التعليمتين. فبينما تأخذ pshufd المسارات المخلوطة من معامل مصدر مختلف، تنفذ pshufb الخلط في مكانه داخل الوجهة.

section .rodata
align 16
reverse: db 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0

section .text
fn:
  pshufb xmm0, [rel reverse]
  ; xmm0[0] = xmm0[reverse[0]] (xmm0[15])
  ; xmm0[1] = xmm0[reverse[1]] (xmm0[14])
  ; ...
  ; in the end, the bytes in xmm0 are reversed

pshufb أكثر مرونة من ذلك: فهي تستطيع تصفير أي مسار. إذا كانت البتة العليا مضبوطة في المسار i من متجه التحكم، فسيُصفَّر المسار i-th في الوجهة. وهذا يجعل pshufb عملية تبديل بايتات عشوائية ومسحًا انتقائيًا في تعليمة واحدة.

ولأن pshufb تعمل على مستوى البايت، يمكن استخدامها أيضًا لخلط مسارات بأحجام مختلفة، بتجميع المسارات المتجاورة معًا. على سبيل المثال، يمكن استخدامها لخلط الكلمات المزدوجة (dwords):

section .rodata
align 16
input: dd 13, 25, 37, 49 ; each number takes 4 bytes
reverse_dwords: db 12, 13, 14, 15, 8, 9, 10, 11, 4, 5, 6, 7, 0, 1, 2, 3 ; aligned too, since input spans 16 bytes

section .text
fn:
  movdqa xmm0, [rel input] ; xmm0 = {13, 25, 37, 49}
  pshufb xmm0, [rel reverse_dwords]
  ; xmm0 = {49, 37, 25, 13}

تداخل المسارات

تأخذ تعليمات فك الحزمة مسارات من معاملين وتنسجها معًا بالتناوب بينهما. وهناك صيغتان: واحدة للأعداد الصحيحة وأخرى للأعداد ذات الفاصلة العائمة، وهما تتبعان إلى حد كبير البنية النحوية العامة نفسها، مع فارقين:

  1. توجد لاحقة l أو h للإشارة إلى ما إذا كانت تعمل على النصف الأدنى (l) أو النصف الأعلى (h) من كل معامل.
  2. تأخذ الصيغتان الخاصتان بالأعداد الصحيحة لاحقتي حجم، تمثل الثانية ضعف الأولى. على سبيل المثال، bw أو qdq (لاحقة الحجم لـ 16 بايت هي dq، كما في movdqu).
punpcklwd xmm0, xmm1 ; p + unpck + l + wd
; this interleaves the first 4 words of xmm0 and xmm1 into 4 dwords
; for each dword in the result, the first word is taken from xmm0 and the second, from xmm1
; xmm0 = {xmm0[0], xmm1[0], xmm0[1], xmm1[1], xmm0[2], xmm1[2], xmm0[3], xmm1[3]}

unpckhps xmm2, xmm3 ; unpck + h + p + s
; this interleaves the high 32-bit floats of xmm2 and xmm3
; xmm2 = {xmm2[2], xmm3[2], xmm2[3], xmm3[3]}

التضييق مع التشبع

تسير تعليمات الحزم في الاتجاه المعاكس، إذ تدمج مسارات معاملين في مسارات بعرض نصف العرض في الوجهة.

لا تأخذ هذه التعليمات بادئة p. وما عدا ذلك، تجمع الصيغة عناصر رأيناها من قبل:

  • العملية المنفذة، pack.
  • حرف s أو u للإشارة إلى ما إذا كانت قيم الخرج موقّعة أم غير موقّعة، على الترتيب.
  • حرف s يعني التشبع، كما في الحساب المتشبع الذي رأيناه في مفهوم سابق.
  • لاحقتا حجم، تشير الثانية إلى نصف عرض الأولى (وهو السلوك المعاكس لـ unpck).

ولأن عملية الحزم تضيِّق، فهي لا تحتاج إلى لاحقة l أو h:

packssdw xmm0, xmm1 ; pack + s (for signed) + s (for saturating) + dw (dword to word)
packuswb xmm2, xmm3 ; pack + u (for unsigned) + s (for saturating) + wb (word to byte)

هذه التعليمات متشبعة، أي أن قيم الخرج تُقيَّد ضمن النطاق الأضيق. وهي تعمل من الكلمة المزدوجة إلى الكلمة ومن الكلمة إلى البايت. ولا توجد صيغة من الكلمة الرباعية إلى الكلمة المزدوجة.

لاحظ أن المدخل يُفسَّر دائمًا على أنه موقّع. أما النوع، موقّعًا كان أم غير موقّع، فهو للـ_خرج_. فهو يحدد النطاق الذي ستُقيَّد فيه القيم. على سبيل المثال، تقيّد packsswb القيم ضمن نطاق بايت موقّع، أي [-128, 127].

تأتي مسارات النتيجة الدنيا من معامل الوجهة والعليا من المصدر:

packusdw xmm0, xmm1   ; 8 words, each clamped to 0..65535
; xmm0 = {xmm0[0], xmm0[1], xmm0[2], xmm0[3], xmm1[0], xmm1[1], xmm1[2], xmm1[3]}

لا يوجد نظير لهذه التعليمات في الأعداد ذات الفاصلة العائمة.

نقل المسارات الفردية

حتى الآن، كنا دائمًا ننقل البيانات بين سجل SIMD وسجل الأغراض العامة باستخدام movq/movd. تستطيع هاتان التعليمتان الكتابة إلى المسار الأدنى من سجل SIMD أو القراءة منه فقط، وعند الكتابة تصفّران كل المسارات الأخرى.

وهناك تعليمات تفعل الشيء نفسه مع أي مسار، وليس الأول فقط، مع ترك المسارات الأخرى دون مساس:

  • تعليمات الإدراج تكتب عنصرًا واحدًا من سجل الأغراض العامة، أو من الذاكرة، في مسار يُختار بقيمة فورية.
  • تعليمات الاستخراج تقرأ من مسار واحد إلى سجل الأغراض العامة.

وكلتاهما تتبعان صيغة الأعداد الصحيحة: p + insr/extr + لاحقة الحجم (b أو w أو d أو q).

وفي كلتا الحالتين، يكون سجل الأغراض العامة عادةً بعرض 32 بت. تحتاج pinsrq و pextrq فقط إلى معامل من 64 بت. أما معامل الذاكرة فحجمه دائمًا بحجم العملية: 8 بتات لـ pinsrb/pextrb، و16 بتًا لـ pinsrw/pextrw، وهكذا.

pinsrb xmm0, eax, 5        ; replace byte 5 of xmm0 with the low byte of eax
pextrb byte [rdx], xmm0, 5 ; copy byte 5 of xmm0 into the memory location indicated by rdx

لاحظ أن هذه التعليمات، مثل movd و movq تمامًا، تنقل بايتات خامًا فقط. وهذا يعني أنه يمكن استخدامها أيضًا مع قيم الفاصلة العائمة المخزنة في الذاكرة أو في سجل الأغراض العامة.

التعليمات

أنت تكتب الحلقات الداخلية لخط معالجة صور برمجي، أي المرحلة التي تُهيّئ الأنسجة وتُركّب الطبقات قبل وصولها إلى الشاشة. يعمل خط المعالجة على البكسلات كتلةً كتلة، مطبّقًا العملية نفسها على الكتلة بأكملها.

يتكوّن البكسل من أربع قنوات بحجم بايت واحد: الأحمر والأخضر والأزرق والألفا، بهذا الترتيب (RGBA). وتتكوّن الكتلة من 4 بكسلات، أي 16 بايتًا في المجموع.

لديك خمس مهام.

تستقبل المُدخَلات عبر عناوين في الذاكرة، وتكتب ناتجك عبر عنوان النتيجة. جميع عناوين الذاكرة في هذا التمرين محاذاة على 16 بايت.

Note

يجب إجراء الحسابات في هذا التمرين باستخدام تعليمات SIMD، لا بعمليات مُفردة.

1. تحويل صورة إلى ترتيب العرض

تُخزَّن الأنسجة بترتيب RGBA، لكن ذاكرة الإطار التي يرسم فيها خط المعالجة هذا تتوقّع كل بكسل بترتيب BGRA: قناتا الأحمر والأزرق مبدَّلتان، وقناتا الأخضر والألفا في مكانهما. تصل الصورة على شكل تتابع من الكتل، وتُحوَّل كل كتلة بالطريقة نفسها.

نفّذ الدالة to_display_order، التي تحوّل صورة كاملة من RGBA إلى BGRA، كتلةً كتلة. ينبغي أن تعرّف قناع التحكم في إعادة ترتيب القنوات كثابت مُعبّأ في الذاكرة، وتعيد استخدامه مع كل كتلة.

تستقبل هذه الدالة الوسائط التالية بهذا الترتيب:

  • result: عنوان ذاكرة لمخزن مؤقت تُكتب فيه الكتل المحوَّلة، 16 بايتًا لكل كتلة.
  • pixels: عنوان ذاكرة كتل المصدر، 4 بكسلات لكل كتلة، وكل بكسل 4 بايتات بترتيب RGBA.
  • block_count: عدد الكتل، وهو دائمًا أكبر من 0.
pixels      = {200, 64, 32, 255, 10, 20, 30, 40, 0, 0, 0, 255, 12, 34, 56, 78,
               1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16} // 2 blocks
block_count = 2
result      = {32, 64, 200, 255, 30, 20, 10, 40, 0, 0, 0, 255, 56, 34, 12, 78,
               3, 2, 1, 4, 7, 6, 5, 8, 11, 10, 9, 12, 15, 14, 13, 16}

لا تُرجع هذه الدالة أي قيمة.

2. ملء منطقة بلون واحد

لمسح منطقة أو طلاء مقطع مستوٍ، يكتب خط المعالجة لونًا واحدًا على كل بكسل في المنطقة.

نفّذ الدالة fill_region، التي تملأ منطقة من block_count كتلة بنُسخ من لون واحد.

تستقبل هذه الدالة الوسائط التالية بهذا الترتيب:

  • result: عنوان ذاكرة لمخزن مؤقت تُكتب فيه الكتل المملوءة، 16 بايتًا لكل كتلة.
  • color: عنوان ذاكرة لبكسل واحد، 4 بايتات بترتيب RGBA.
  • block_count: عدد الكتل المراد ملؤها، وهو دائمًا أكبر من 0.
color       = {18, 52, 86, 120}
block_count = 2
result      = {18, 52, 86, 120, 18, 52, 86, 120, 18, 52, 86, 120, 18, 52, 86, 120,
               18, 52, 86, 120, 18, 52, 86, 120, 18, 52, 86, 120, 18, 52, 86, 120}

لا تُرجع هذه الدالة أي قيمة.

3. تشبيك خطَّي مسح

يلزم دمج طبقتين كل منهما بقناة واحدة في مخزن مؤقت واحد مع تشبيك عيّناتهما. يتناوب الناتج عيّنة من الطبقة الأولى ثم عيّنة من الثانية.

نفّذ الدالة weave_scanlines، التي تشبك صفّين يتكوّن كل منهما من 16 عيّنة في صف واحد من 32 عيّنة.

تستقبل هذه الدالة الوسائط التالية بهذا الترتيب:

  • result: عنوان ذاكرة لمخزن مؤقت تُكتب فيه العيّنات المشبَّكة الـ32.
  • first: عنوان ذاكرة الصف الأول، 16 عيّنة، كل عيّنة قيمة من 8 بتات.
  • second: عنوان ذاكرة الصف الثاني، 16 عيّنة، كل عيّنة قيمة من 8 بتات.
first  = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15}
second = {100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115}
result = {0, 100, 1, 101, 2, 102, 3, 103, 4, 104, 5, 105, 6, 106, 7, 107,
          8, 108, 9, 109, 10, 110, 11, 111, 12, 112, 13, 113, 14, 114, 15, 115}

لا تُرجع هذه الدالة أي قيمة.

4. تعبئة صف مُفتَّح

تُعيد مرحلة زيادة السطوع تحجيم كل عيّنة بدقة عمل تبلغ 16 بت، بحيث يمكن لعيّنة شديدة السطوع أن تتجاوز 255، ويمكن لفرق أن ينخفض دون 0. تُضيّق المرحلة الأخيرة قيم العمل تلك إلى عيّنات من 8 بتات للعرض، فتحدّ أي قيمة دون 0 عند 0، وأي قيمة فوق 255 عند 255.

نفّذ الدالة pack_samples، التي تُضيّق مجموعتين من 8 قيم عمل إلى صف واحد من 16 عيّنة، بالترتيب.

تستقبل هذه الدالة الوسائط التالية بهذا الترتيب:

  • result: عنوان ذاكرة لمخزن مؤقت تُكتب فيه العيّنات الـ16 المحدودة، كل عيّنة قيمة من 8 بتات.
  • first: عنوان ذاكرة أول 8 قيم عمل، كل قيمة عدد صحيح مُوقَّع بطول 16 بت.
  • second: عنوان ذاكرة الـ8 قيم العمل التالية، كل قيمة عدد صحيح مُوقَّع بطول 16 بت.
first  = {300, -5, 128, 255, 0, 400, 64, 200}
second = {255, 256, -1, 100, 50, 1000, 7, 0}
result = {255, 0, 128, 255, 0, 255, 64, 200,   255, 255, 0, 100, 50, 255, 7, 0}

لا تُرجع هذه الدالة أي قيمة.

5. فصل الإحداثيات إلى x و y

غالبًا ما تصل بيانات الأنسجة والرؤوس مشبَّكة، بحيث يكون x و y لكل نقطة معبَّأين معًا. ومع ذلك، غالبًا ما يكون من الضروري فصلهما للمعالجة بكفاءة: كل قيم x في متجه واحد، وكل قيم y في متجه آخر.

نفّذ الدالة split_coordinates، التي تفصل أربع نقط (x, y) مشبَّكة إلى متجه من إحداثيات x ومتجه من إحداثيات y.

تستقبل هذه الدالة الوسائط التالية بهذا الترتيب:

  • xs: عنوان ذاكرة لمخزن مؤقت تُكتب فيه إحداثيات x الأربعة، 4 أعداد ذات فاصلة عائمة بطول 32 بت.
  • ys: عنوان ذاكرة لمخزن مؤقت تُكتب فيه إحداثيات y الأربعة، 4 أعداد ذات فاصلة عائمة بطول 32 بت.
  • first: عنوان ذاكرة النقطتين الأولى والثانية، 4 أعداد ذات فاصلة عائمة بطول 32 بت، بالشكل {x0, y0, x1, y1}.
  • second: عنوان ذاكرة النقطتين التاليتين، 4 أعداد ذات فاصلة عائمة بطول 32 بت، بالشكل {x2, y2, x3, y3}.
first  = {0.0, 0.5, 1.0, 1.5} // {x0, y0, x1, y1}
second = {2.0, 2.5, 3.0, 3.5} // {x2, y2, x3, y3}
xs     = {0.0, 1.0, 2.0, 3.0} // {x0, x1, x2, x3}
ys     = {0.5, 1.5, 2.5, 3.5} // {y0, y1, y2, y3}

لا تُرجع هذه الدالة أي قيمة.

تعديل عبر GitHub يفتح الرابط في نافذة أو علامة تبويب جديدة
x86-64 Assembly Exercism

مستعد لبدء تبديل القنوات؟

سجّل في Exercism لتتعلّم وتتقن x86-64 Assembly عبر 22 مفهومًا130 تمرينًا، وإرشاد بشري حقيقي، وكل ذلك مجانًا.