
مرحبا يا هبر! في هذه المقالة ، أود أن أتحدث عن عمل النقطة العائمة للمعالجات ذات هندسة ARM. أعتقد أن هذه المقالة ستكون مفيدة في المقام الأول لأولئك الذين
ينقلون نظام التشغيل الخاص بهم إلى بنية ARM وفي نفس الوقت يحتاجون إلى دعم لنقطة عائمة الأجهزة (وهو ما فعلناه لـ
Embox ، والذي استخدم سابقًا تنفيذ برنامج لعمليات النقطة العائمة).
لذلك دعونا نبدأ.
أعلام المترجم
لدعم النقطة العائمة ، يجب عليك تمرير الأعلام الصحيحة إلى المترجم. يؤدي غوغل سريع منا إلى فكرة أن خيارين مهمان بشكل خاص: -mfloat-abi و -mfpu. يقوم الخيار -mfloat-abi بتعيين
ABI لعمليات الفاصلة العائمة ويمكن أن يحتوي على إحدى القيم الثلاث: "soft" و "softfp" و "hard". خيار "soft" ، كما يوحي الاسم ، يخبر المترجم باستخدام استدعاءات الدوال المضمنة لبرمجة النقطة العائمة (تم استخدام هذا الخيار من قبل). سيتم النظر في الاثنين المتبقيين من softfp و "hard" بعد ذلك بقليل ، بعد النظر في خيار -mfpu.
-Mfpu flag وإصدار VFP
يتيح لك خيار -mfpu ، كما هو مكتوب في
وثائق مجلس التعاون الخليجي عبر الإنترنت ، تحديد نوع الجهاز ويمكنه اتخاذ الخيارات التالية:
"تلقائي" ، "vfpv2" ، "vfpv3" ، "vfpv3-fp16" ، "vfpv3-d16" ، "vfpv3-d16-fp16" ، "vfpv3xd" ، "vfpv3xd-fp16" ، "neon-vfpv3" ، "نيون" -fp16 '،' vfpv4 '،' vfpv4-d16 '،' fpv4-sp-d16 '،' neon-vfpv4 '،' fpv5-d16 '،' fpv5-sp-d16 '،' fp-armv8 '،' neon -fp-armv8 'و' crypto-neon-fp-armv8 '. و "نيون" هو نفسه "neon-vfpv3" و "vfp" هو "vfpv2".
المترجم (arm-none-eabi-gcc (15: 5.4.1 + svn241155-1) 5.4.1 20160919) ينتج قائمة مختلفة قليلاً ، لكن هذا لا يغير الجوهر. على أي حال ، نحن بحاجة إلى فهم كيفية تأثير هذه العلامة أو تلك على المترجم ، وبالطبع ، أي إشارة يجب استخدامها متى.
لقد بدأت في فهم النظام الأساسي بناءً على معالج imx6 ، لكننا سنؤجله لفترة قصيرة ، نظرًا لأن معالج النيون لديه ميزات سأناقشها لاحقًا ، ونبدأ بحالة أبسط - من منصة
التكامل / cp ،
ليس لدي اللوحة نفسها ، لذلك تم تصحيح الأخطاء في محاكي qemu. في qemu ، تعتمد منصة Interator / cp على معالج
ARM926EJ-S ، والذي بدوره يدعم المعالج
VFP9-S . يتوافق هذا المعالج المشترك مع الإصدار 2 من بنية النقطة العائمة المتجهات (VFPv2). وفقًا لذلك ، تحتاج إلى تعيين -mfpu = vfpv2 ، لكن هذا الخيار لم يكن في قائمة خيارات المترجم الخاص بي. على
الإنترنت ، قابلت خيار تجميع مع الأعلام -mcpu = arm926ej-s -mfpu = vfpv3-d16 ، وقمت بتثبيته ، وتم تجميع كل شيء من أجلي. عندما بدأت ، تلقيت
استثناء تعليمات غير محدد ، والذي كان متوقعًا ، لأنه تم
إيقاف تشغيل المعالج.
لتمكين المعالج المشترك من العمل ، تحتاج إلى تعيين بت EN [30] في سجل
FPEXC . يتم ذلك باستخدام الأمر VMSR.
asm volatile ("VMSR FPEXC, %0" : : "r" (1 << 30);
في الواقع ، يتم معالجة الأمر VMSR بواسطة المعالج ويطرح استثناء إذا لم يتم تشغيل المعالج ، ولكن الوصول إلى هذا التسجيل
لا يسبب ذلك . صحيح ، على عكس الآخرين ، لا يمكن الوصول إلى هذا السجل إلا في
الوضع المميز .
بعد أن سمح للمعالج بالعمل ، بدأت اختباراتنا للوظائف الرياضية بالنجاح. ولكن عندما قمت بتشغيل التحسين (-O2) ، تم رفع استثناء التعليمات غير المحدد المذكور سابقًا. ونشأ على تعليمات
vmov التي تم استدعاؤها في التعليمات البرمجية سابقًا ، ولكن تم تنفيذها بنجاح (بدون استثناء). أخيرًا ، وجدت في نهاية الصفحة عبارة "التعليمات التي تنسخ الثوابت الفورية متاحة في VFPv3" (أي ، العمليات مع الثوابت مدعومة بدءًا من VFPv3). وقررت التحقق من الإصدار الذي تم إصداره في محاكي الخاص بي. يتم تسجيل النسخة في سجل
FPSID . ويترتب على الوثائق أن قيمة التسجيل يجب أن تكون 0x41011090. هذا يقابل 1 في مجال العمارة [19..16] أي VFPv2. في الواقع ، بعد أن قمت بعمل نسخة مطبوعة عند بدء التشغيل ، حصلت على هذا
unit: initializing embox.arch.arm.fpu.vfp9_s: VPF info: Hardware FP support Implementer = 0x41 (ARM) Subarch: VFPv2 Part number = 0x10 Variant = 0x09 Revision = 0x00
بعد أن قرأت بعناية أن "vfp" هو اسم مستعار "vfpv2" ، قمت بتعيين العلامة الصحيحة ، لقد نجحت. بالعودة إلى
الصفحة حيث رأيت مجموعة الأعلام -mcpu = arm926ej-s -mfpu = vfpv3-d16 ، ألاحظ أنني لم أكن حذراً بما فيه الكفاية ، لأن -mfloat-abi = soft يظهر في قائمة الأعلام. أي أنه لا يوجد دعم للأجهزة في هذه الحالة. بتعبير أدق ، لا يهم -mfpu إلا إذا تم تعيين قيمة أخرى غير "soft" على -mfloat-abi.
المجمع
حان الوقت للحديث عن المجمع. بعد كل شيء ، كنت بحاجة إلى تقديم دعم وقت التشغيل ، وعلى سبيل المثال ، المترجم بالطبع لا يعرف عن تبديل السياق.
يسجل
لنبدأ بوصف السجلات. يسمح لك VFP بإجراء العمليات باستخدام أرقام الفاصلة العائمة 32 بت (s0..s31) و 64 بت (d0..d15). تظهر المراسلات بين هذه السجلات في الصورة أدناه.

Q0-Q15 عبارة عن تسجيلات 128 بت من الإصدارات القديمة للعمل مع SIMD ، والمزيد عنها لاحقًا.
نظام القيادة
بالطبع ، في معظم الأحيان يجب إعطاء العمل مع سجلات VFP للمترجم ، ولكن على الأقل يجب عليك كتابة تبديل السياق يدويًا. إذا كان لديك بالفعل فهم تقريبي لصيغة تعليمات المجمّع للعمل مع سجلات الأغراض العامة ، فلا يجب أن يكون التعامل مع التعليمات الجديدة صعبًا. في أغلب الأحيان ، تُضاف البادئة "v".
vmov d0, r0, r1 /* r0 r1, .. d0 64 , r0-1 32 */ vmov r0, r1, d0 vadd d0, d1, d2 vldr d0, r0 vstm r0!, {d0-d15} vldm r0!, {d0-d15}
وهكذا دواليك. يمكن العثور على قائمة كاملة بالأوامر على
موقع ARM .
وبالطبع ، لا تنسى إصدار VFP بحيث لا توجد حالات مثل تلك الموضحة أعلاه.
Flag -mfloat-abi 'softfp' و 'hard'
عودة إلى -mfloat-abi. إذا قرأت
الوثائق ، فسوف نرى:
يسمح "softfp" بإنشاء التعليمات البرمجية باستخدام تعليمات الفاصلة العائمة للأجهزة ، ولكنه لا يزال يستخدم اصطلاحات الاتصال العائمة. "صعب" يسمح بإنشاء تعليمات الفاصلة العائمة ويستخدم اصطلاحات الاتصال الخاصة بـ FPU.
أي أننا نتحدث عن تمرير الحجج إلى دالة. ولكن على الأقل لم يكن واضحًا بالنسبة لي ما هو الفرق بين اصطلاحات الاتصال "الناعمة - العائمة" و "الخاصة بـ FPU". بافتراض أن الحالة الصلبة تستخدم تسجيلات النقطة العائمة ، وأن الحالة softfp تستخدم التسجيلات الصحيحة ، فقد وجدت تأكيدًا على موقع
debian wiki . وعلى الرغم من أن هذا هو لمعالجات NEON ، ولكن لا يهم. نقطة أخرى مثيرة للاهتمام هي أنه مع خيار softfp ، يمكن للمترجم ، ولكن ليس مطلوبًا لاستخدام دعم الأجهزة:
"يمكن للمترجم اتخاذ خيارات ذكية حول متى وما إذا كان يولد تعليمات FPU محاكاة أو حقيقية بناءً على نوع FPU المختار (-mfpu =)"
للحصول على وضوح أفضل ، قررت أن أجرب ، وفوجئت للغاية ، لأنه مع إيقاف تشغيل تحسين O0 ، كان الفرق طفيفًا جدًا ولم ينطبق على الأماكن التي تم فيها استخدام النقطة العائمة بالفعل. تخمين أن المترجم يدفع فقط كل شيء على المكدس ، بدلاً من استخدام التسجيلات ، قمت بتشغيل التحسين -O2 وفوجئت مرة أخرى ، لأنه مع التحسين بدأ المترجم باستخدام تسجيلات النقطة العائمة للأجهزة لكل من الخيارات الثابتة وخيارات sotffp ، والفرق هو كيف وفي حالة -O0 كان ذلك ضئيلاً للغاية. ونتيجة لذلك ، بالنسبة لي ، شرحت ذلك بحقيقة أن المترجم يحل
المشكلة المرتبطة بحقيقة أنه إذا قمت بنسخ البيانات بين تسجيلات الفاصلة العائمة والأعداد الصحيحة ، فسوف ينخفض الأداء بشكل كبير. ويبدأ المترجم ، عند التحسين ، في استخدام جميع الموارد المتاحة له.
عندما سُئلت عن أي إشارة لاستخدام "softfp" أو "صلب" ، أجبت بنفسي على النحو التالي: حيثما توجد بالفعل أجزاء مجمعة بعلامة "softfp" ، يجب عليك استخدام "صلب". إذا كان هناك أي منها ، فأنت بحاجة إلى استخدام softfp.
تبديل السياق
نظرًا لأن Embox يدعم المهام المتعددة الوقائية ، للعمل بشكل صحيح في وقت التشغيل ، بطبيعة الحال ، كان هناك حاجة إلى تنفيذ تبديل السياق. لهذا ، من الضروري حفظ سجلات المعالج المشترك. هناك زوجان من الفروق الدقيقة. أولاً: اتضح أن
أوامر عملية التكديس للنقاط العائمة (vstm / vldm) لا تدعم جميع الأوضاع . ثانيًا: لا تدعم هذه العمليات العمل مع أكثر من ستة عشر تسجيلًا 64 بت. إذا كنت بحاجة إلى تحميل / حفظ المزيد من السجلات في وقت واحد ، فأنت بحاجة إلى استخدام تعليماتين.
سأقدم تحسين صغير آخر. في الواقع ، لا يعد حفظ 256 بايت من سجلات VFP واستعادتها ضروريًا في كل مرة (تشغل سجلات الأغراض العامة 64 بايت فقط ، لذا فإن الفرق كبير). لن يؤدي التحسين الواضح إلى تنفيذ هذه العمليات إلا إذا كانت العملية تستخدم هذه السجلات من حيث المبدأ.
كما ذكرت بالفعل ، عند إيقاف تشغيل معالج VFP ، ستؤدي محاولة تنفيذ التعليمات المقابلة إلى استثناء "تعليمات غير محددة". في معالج هذا الاستثناء ، تحتاج إلى التحقق من سبب الاستثناء ، وإذا كان الأمر يتعلق باستخدام معالج VPF ، يتم تمييز العملية على أنها تستخدم معالج VFP المشترك.
ونتيجة لذلك ، تم استكمال حفظ / استعادة السياق المكتوب بالفعل بوحدات الماكرو
#define ARM_FPU_CONTEXT_SAVE_INC(tmp, stack) \ vmrs tmp, FPEXC ; \ stmia stack!, {tmp}; \ ands tmp, tmp, #1<<30; \ beq fpu_out_save_inc; \ vstmia stack!, {d0-d15}; \ fpu_out_save_inc: #define ARM_FPU_CONTEXT_LOAD_INC(tmp, stack) \ ldmia stack!, {tmp}; \ vmsr FPEXC, tmp; \ ands tmp, tmp, #1<<30; \ beq fpu_out_load_inc; \ vldmia stack!, {d0-d15}; \ fpu_out_load_inc:
للتحقق من صحة عملية تبديل السياق في ظروف الفاصلة العائمة ، كتبنا اختبارًا نقوم فيه بالضرب في خيط واحد في حلقة ونقسمه في آخر ، ثم مقارنة النتائج.
EMBOX_TEST_SUITE("FPU context consistency test. Must be compiled with -02"); #define TICK_COUNT 10 static float res_out[2][TICK_COUNT]; static void *fpu_context_thr1_hnd(void *arg) { float res = 1.0f; int i; for (i = 0; i < TICK_COUNT; ) { res_out[0][i] = res; if (i == 0 || res_out[1][i - 1] > 0) { i++; } if (res > 0.000001f) { res /= 1.01f; } sleep(0); } return NULL; } static void *fpu_context_thr2_hnd(void *arg) { float res = 1.0f; int i = 0; for (i = 0; i < TICK_COUNT; ) { res_out[1][i] = res; if (res_out[0][i] != 0) { i++; } if (res < 1000000.f) { res *= 1.01f; } sleep(0); } return NULL; } TEST_CASE("Test FPU context consistency") { pthread_t threads[2]; pthread_t tid = 0; int status; status = pthread_create(&threads[0], NULL, fpu_context_thr1_hnd, &tid); if (status != 0) { test_assert(0); } status = pthread_create(&threads[1], NULL, fpu_context_thr2_hnd, &tid); if (status != 0) { test_assert(0); } pthread_join(threads[0], (void**)&status); pthread_join(threads[1], (void**)&status); test_assert(res_out[0][0] != 0 && res_out[1][0] != 0); for (int i = 1; i < TICK_COUNT; i++) { test_assert(res_out[0][i] < res_out[0][i - 1]); test_assert(res_out[1][i] > res_out[1][i - 1]); } }
اجتاز الاختبار بنجاح عند إيقاف تشغيل التحسين ، لذلك أوضحنا في وصف الاختبار أنه يجب أن يتم ترجمته مع التحسين ، EMBOX_TEST_SUITE ("اختبار اتساق سياق FPU. يجب أن يتم ترجمته مع -02") ؛ على الرغم من أننا نعلم أن الاختبارات يجب ألا تعتمد على ذلك.
المعالج المشترك NEON و SIMD
حان الوقت لمعرفة سبب تأجيل قصة imx6. والحقيقة هي أنها تستند إلى نواة Cortex-A9 وتحتوي على معالج NEON المشترك الأكثر تقدمًا (https://developer.arm.com/technologies/neon). NEON ليس فقط VFPv3 ، ولكنه أيضًا معالج SIMD. تستخدم VFP و NEON نفس السجلات. يستخدم VFP تسجيلات 32 بت و 64 بت للتشغيل ، ويستخدم NEON تسجيلات 64 بت و 128 بت ، وقد تم تعيين هذا الأخير فقط Q0-Q16. بالإضافة إلى القيم الصحيحة وأرقام الفاصلة العائمة ، فإن NEON قادرة أيضًا على العمل مع حلقة متعددة الحدود من الدرجة 16 أو 8 من المعامل 2.
لا يختلف وضع vfp لـ NEON تقريبًا عن المعالج المساعد vfp9-s المفكك. بالطبع ، من الأفضل تحديد خيارات vfpv3 أو vfpv3-d32 لـ -mfpu لتحسين أفضل ، نظرًا لأنه يحتوي على 32 تسجيلًا 64 بت. ولتمكين المعالج المشترك ، يجب منح الوصول إلى المعالجات المشتركة c10 و c11. يتم ذلك باستخدام الأوامر
asm volatile ("mrc p15, 0, %0, c1, c0, 2" : "=r" (val) :); val |= 0xf << 20; asm volatile ("mcr p15, 0, %0, c1, c0, 2" : : "r" (val));
ولكن لا توجد اختلافات جوهرية أخرى.
شيء آخر إذا حددت -mfpu = neon ، في هذه الحالة يمكن للمترجم استخدام تعليمات SIMD.
باستخدام SIMD في C
من أجل << تسجيل >> القيم يدويًا عن طريق التسجيل ، يمكنك تضمين “arm_neon.h” واستخدام أنواع البيانات المقابلة:
float32x4_t لأربعة عوامات 32 بت في تسجيل واحد ، uint8x8_t لثمانية أعداد صحيحة 8 بت وهكذا. للوصول إلى قيمة واحدة ، نشير إليها على أنها صفيف ، إضافة ، ضرب ، تعيين ، إلخ. أما المتغيرات العادية فمثلا:
uint32x4_t a = {1, 2, 3, 4}, b = {5, 6, 7, 8}; uint32x4_t c = a * b; printf(“Result=[%d, %d, %d, %d]\n”, c[0], c[1], c[2], c[3]);
بالطبع ، يعد استخدام ناقل البيانات التلقائي أسهل. للتوجيه التلقائي ، أضف علامة -ftree-vectorize إلى GCC.
void simd_test() { int a[LEN], b[LEN], c[LEN]; for (int i = 0; i < LEN; i++) { a[i] = i; b[i] = LEN - i; } for (int i = 0; i < LEN; i++) { c[i] = a[i] + b[i]; } for (int i = 0; i < LEN; i++) { printf("c[i] = %d\n", c[i]); } }
تقوم حلقة الإضافة بإنشاء التعليمات البرمجية التالية:
600059a0: f4610adf vld1.64 , [r1 :64] 600059a4: e2833010 add r3, r3, #16 600059a8: e28d0a03 add r0, sp, #12288 ; 0x3000 600059ac: e2811010 add r1, r1, #16 600059b0: f4622adf vld1.64 , [r2 :64] 600059b4: e2822010 add r2, r2, #16 600059b8: f26008e2 vadd.i32 q8, q8, q9 600059bc: ed430b04 vstr d16, [r3, #-16] 600059c0: ed431b02 vstr d17, [r3, #-8] 600059c4: e1530000 cmp r3, r0 600059c8: 1afffff4 bne 600059a0 <foo+0x58> 600059cc: e28d5dbf add r5, sp, #12224 ; 0x2fc0 600059d0: e2444004 sub r4, r4, #4 600059d4: e285503c add r5, r5, #60 ; 0x3c
بعد إجراء اختبارات للشفرة المتوازية ، وجدنا أن الإضافة البسيطة في حلقة ، شريطة أن تكون المتغيرات مستقلة ، تعطي التسارع 7 مرات. بالإضافة إلى ذلك ، قررنا معرفة مدى تأثير التوازي على المهام الحقيقية ، وأخذنا MESA3d بمحاكاة برامجها وقياس عدد الإطارات في الثانية بأعلام مختلفة ، وحصلنا على مكسبين من الإطارات في الثانية (15 مقابل 13) ، أي أن التسارع هو حوالي 15-20٪ .
سأعطي مثالاً آخر
على التسريع باستخدام أوامر NEON ، وليس أوامرنا ، ولكن من ARM.
نسخ الذاكرة أسرع بنسبة تصل إلى 50 بالمائة عن المعتاد. الأمثلة الحقيقية موجودة في المجمع.
دورة النسخ العادية:
WordCopy LDR r3, [r1], #4 STR r3, [r0], #4 SUBS r2, r2, #4 BGE WordCopy
حلقة بأوامر وسجلات نيون:
NEONCopyPLD PLD [r1, #0xC0] VLDM r1!,{d0-d7} VSTM r0!,{d0-d7} SUBS r2,r2,#0x40 BGE NEONCopyPLD
من الواضح أن النسخ بمقدار 64 بايت أسرع من 4 بايت ، وهذا النسخ سيعطي زيادة بنسبة 10٪ ، لكن الـ 40٪ المتبقية يبدو أنها تعطي عمل المعالج المشترك.
اللحاء م
لا يختلف العمل مع وحدات FPU في Cortex-M كثيرًا عن ذلك الموصوف أعلاه. على سبيل المثال ، هذه هي الطريقة التي يبدو بها الماكرو أعلاه لحفظ سياق fpu-shny
#define ARM_FPU_CONTEXT_SAVE_INC(tmp, stack) \ ldr tmp, =CPACR; \ ldr tmp, [tmp]; \ tst tmp, #0xF00000; \ beq fpu_out_save_inc; \ vstmia stack!, {s0-s31}; fpu_out_save_inc:
أيضا ، يستخدم الأمر vstmia فقط التسجيلات s0-s31 ويتم الوصول إلى سجلات التحكم بشكل مختلف. لذلك ، لن أخوض في الكثير من التفاصيل ، سأشرح فقط فرق. لذا ، قدمنا دعمًا لـ STM32F7discovery مع
cortex-m7 لذلك ، على التوالي ، نحتاج إلى تعيين علامة -mfpu = fpv5-sp-d16. يرجى ملاحظة أنه في الإصدارات المحمولة ، تحتاج إلى النظر عن كثب في إصدار المعالج المشترك ، حيث قد تحتوي نفس القشرة-م على خيارات مختلفة. لذلك ، إذا لم يكن الخيار الخاص بك بدقة مزدوجة ، ولكن بدقة واحدة ، فقد لا يكون هناك تسجيلات D0-D16 ، كما هو الحال في
stm32f4discovery ، وهذا هو السبب في استخدام المتغير مع السجلات S0-S31. بالنسبة لوحدة التحكم هذه ، نستخدم -mfpu = fpv4-sp-d16.
والفرق الرئيسي هو الوصول إلى سجلات التحكم الخاصة بوحدة التحكم ، فهي تقع مباشرة في مساحة العنوان للنواة الرئيسية ، ولأنواع مختلفة فهي مختلفة
cortex-m4 لقشرة m7 .
الخلاصة
بهذا سأختتم قصتي القصيرة حول النقطة العائمة لـ ARM. ألاحظ أن وحدات التحكم الدقيقة الحديثة قوية جدًا ومناسبة ليس فقط للتحكم ، ولكن أيضًا لمعالجة الإشارات أو أنواع مختلفة من معلومات الوسائط المتعددة. من أجل استخدام كل هذه القوة بشكل فعال ، تحتاج إلى فهم كيفية عملها. آمل أن تكون هذه المقالة قد ساعدت على معرفة ذلك بشكل أفضل قليلاً.