تسريع الحسابات باستخدام SSE

كتبه بركات يوم الجمعـة, 25 آذار 2016

SIMD اختصاراً لجملة Single Instruction Multiple Data وتعني تعليمة واحدة تنفذ على عدة بيانات، اقرأ عن تصنيف فلين، تسمح لك SIMD بإجراء عملية حسابية أو منطقية على مجموعة بيانات دفعة واحدة لتسريع هذه الحسابات، هذا النوع من التعليمات مفيد جداً لبعض التطبيقات، خصوصاً التطبيقات الرسومية وتطبيقات الوسائط المتعددة التي تعتمد على إجراء العديد من العملبات الحسابية وعمليات الجبر الخطي كعميات المتجهات والمصفوفات، حيث تمكن SIMD من إجراء هذه الحسابات بشكل متوازي، العديد من المعالجات الرسومية GPUs مبنية وفق هذه المعمارية حيث تسمح بإجراء المئات من العمليات في نفس اللحظة، لكننا سنركز هنا على SIMD التي توفرها المعالجات العادية.

سنتطرق في هذا المقال لمعمارية Streaming SIMD Extensions وتختصر SSE، وهي اضافة SIMD لمعمارية x86 في معالجات أنتل الحديثة، تدعمها أيضاً معالجات AMD نظراً لأنهم يتشاركون نفس المعمارية x86، معالجات ARM الشائعة في الجوالات والكمبيوترات اللوحية لديها اضافة SIMD تسمى NEON، لن نتطرق لها.

فائدة SIMD

حتى تفهم فائدة SSE والـSIMD عموماً، لنأخذ مثال بسيط وشائع، في الألعاب، موقع الشخصية يعبر عن بنقطة $p$ وسرعته في الثانية يعبر عنها بمتجه $\vec{v}$، موقع الشخصية الجديد $p^{'}$ بعد مرور ثانية نحسبه عن طريق جمع النقطة مع المتجه $p^{'} = p + \vec{v}$، لنفترض أن موقعه الحالي $p = (1, 3, 5)$، وسرعته $\vec{v} = \langle 3, 1, 4 \rangle$، فموقعه الجديد بعد مرور الثانية سيساوي $p^{'} = (1 + 3, 3 + 1, 5 + 4)$ أو $p^{'} = (4, 4, 9)$، وذلك عن طريق جمع مركّبات النقطة والمتجه:

p.x = p.x + v.x;
p.y = p.y + v.y;
p.z = p.z + v.z;

هذه الحالة شائعة جداً في التطبيقات الرسومية والألعاب، ففي المثال السابق، أجرينا عملية الجمع باستخدام ثلاثة تعليمات، هنا يأتي دور الـSSE، حيث تساعدك في جمع تلك المركبات في نفس اللحظة وبتعليمه واحدة بدلاً من ثلاث تعليمات لكل عملية جمع، لهذا تسمى SIMD، أي تعليمة واحدة على عدة بيانات، كأنك عرفت مصفوتين وجمعت عناصرها بنفس اللحظة عمودياً (مثال تقريبي فقط):

float p[3] = {1, 3, 5};
float v[3] = {3, 1, 4};

p += v;

فائدتها ستظهر عندما نجري هذه العملية آلاف المرات لعشرات العناصر، حيث سيتضاعف الأداء لعدة مرات.

قبل أن نتعمق، لنأخذ مثال سريع كي تعرف فارق الأداء بين برنامج يعمل بدون SSE عن طريق تمرير الخيار -no-sse لـgcc، حيث يجبر المترجم على استخدام تعليمات الـFPU لإجراء الحسابات، وبرنامج يستخدم تعليمات SSE عن طريق الخيار -msse3 (اخترت SSE3 لأنها أكثر شيوعاً):

#include <stdio.h>

int main(void)
{
  float p[3] = {1, 3, 5};
  float v[3] = {3, 1, 4};
  unsigned long i;

  for(i = 0 ; i < 4294967295UL ; i++)
  {
    p[0] += v[0];
    p[1] += v[1];
    p[2] += v[2];
  }

  printf("p' = <%.2f, %.2f, %.2f>\n", p[0], p[1], p[2]);
  return 0;
}

الآن لنجرب فارق السرعة (معالج أنتل i7-4790k على Window 10 x64):

$ gcc --version
gcc.exe (Rev4, Built by MSYS2 project) 5.2.0
Copyright (C) 2015 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.  There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

$ gcc -Wall -Wextra -pedantic -std=c99 -O3 -mno-sse ex.c
$ time ./a
p' = <67108864.00, 16777216.00, 67108864.00>

real    0m9.919s
user    0m0.000s
sys     0m0.000s

$ gcc -Wall -Wextra -pedantic -std=c99 -O3 -msse3 ex.c
$ time ./a
p' = <67108864.00, 16777216.00, 67108864.00>

real    0m2.990s
user    0m0.000s
sys     0m0.000s

$

البرنامج الأول استغرق 10 ثواني تقريباً لتنفيذ البرنامج، البرنامج الذي يستخدم SSE استغرق 3 ثواني، فرق كبير بمجرد تغيير خيار، انظر هنا لباقي خيارات gcc.

البرنامج السابق لو كتبته باستخدام SSE بشكل صريح سيكون شيء مثل:

#include <stdio.h>
#include <x86intrin.h>

int main(void)
{
  float p[4] __attribute__((aligned(16))) = {1, 3, 5};
  float v[4] __attribute__((aligned(16))) = {3, 1, 4};
  unsigned long i;

  __m128 xmm1 = _mm_load_ps(p);
  __m128 xmm2 = _mm_load_ps(v);

  for(i = 0 ; i < 4294967295UL ; i++)
  {
    xmm1 = _mm_add_ps(xmm1, xmm2);
  }

  _mm_store_ps(p, xmm1);

  printf("p' = <%.2f, %.2f, %.2f>\n", p[0], p[1], p[2]);
  return 0;
}

أو حتى كتابة هذا الجزء من البرنامج بالأسمبلي، إلا أنك غالباً لاتحتاج أكثر اخبار المترجم بأنك تريد استخدام SSE بدل كتابته يدوياً، في الماضي نعم قد تحتاج لكتابة التعليمات يدوياً نظراً لضعف دعم المترجمات لـSSE أو توليدها أكواد ليست بتلك الجودة، لكن الوضع الآن اختلف، فمعظم المترجمات الحديثة تدعم SSE دعم جيد وقد تحسن البرنامج أفضل من لو كتبته يدوياً.

تاريخ أنتل و SIMD

الأجيال الأولى من معالجات أنتل مثل 8086 كانت تدعم العمليات الحسابية والمنطقية على الأعداد الصحيحة فقط، وكانت الأعداد العشرية والدوال الرياضية كالدوال الزواية واللوغارتيمات إما تحاكى عن طريق البرامج أو عن طريق المعالج المساعد 8087، حيث كان معالج مفصول عن المعالج الأساسي، وقدم المعمارية x87 التي توفر تعليمات جديدة لإجراء تلك الحسابات أسرع من محاكاتها باستخدام البرامج، في السابق كنت بحاجة لشراء معالجين أحدهما المعالج الرئيسي والآخر معالج للحسابات الرياضية، السبب عائد لتكلفة التصنيع ذلك الوقت، في بدايات التسعينيات وبعد انخفاض تكاليف تصنيع أشباه الموصلات دمجت أنتل المعالج الرئيسي ومعالج الرياضيات في معالج واحد، الحسابات على الأعداد الصحيحة وباقي وظائف المعالج تتم في وحدة المعالجة الرئيسية CPU، وحسابات الفاصلة العائمة1 والدوال الرياضية تتم في وحدة الفاصلة العائمة FPU، وكلهم على نفس الشريحة في معالج واحد، المهم هنا أن تعرف أن في معمارية x87 هناك 8 مسجلات أسماءها st0 إلى st7 بطول 80 بت لأعداد الفاصلة العائمة، سواءً كنت تستخدم الـfloat بطول 32 بت أو الـdouble بطول 64 بت، فسيحولها المعالج لـ80 بت.

حتى الآن لم تدعم أنتل SIMD، لكن في أواخر التسعينيات عام 1997، أصدرت أنتل أول دعم للـSIMD وذلك عن طريق الإضافة الجديدة لمعمارية x86 والمساة MMX، هذه الإضافة الجديدة كانت أول دعم للـSIMD، حيث قدمت تعليمات جديدة وثمانية مسجلات جديدة أسماءها MMX0 إلى MMX7 بطول 64 بت، لكن هناك عيبين رئيسيين في هذه المعمارية:

  • هذه المسجلات تستخدم نفس مسجلات الـFPU الخاصة بالأعداد العائمة، حيث أنها تستخدم أول 64 بت من الـ80 بت الخاصة بـx87، مما سبب صعوبة في إجراء حسابات عشرية واستخدام MMX في نفس الوقت.
  • العيب الثاني أنها تدعم الأعداد الصحيحة فقط، حيث يمكنك إجراء عمليات على عدد صحيح بطول 64 بت، أو عديدين بطول 32 بت، أو أربعة 16 بت، أو ثمانية بطول 8 بت، إلا أنها لاتدعم الأعداد العشرية الأكثر استخدام في التطبيقات الرسومية.

بعدها بسنة، عام 1998، أطلقت AMD تعديل على MMX اسمها 3DNow، تشبه MMX في أنها تستخدم نفس المسجلات، إلا أن الميزة الأهم فيها أنها تدعم الحسابات باستخدام الفاصلة العائمة، مما يجعلها أكثر فائدة من MMX الأصلية، إلا أنها ورثت من MMX مشكلة مشاركة المسجلات مع الـFPU، يرجى الانتباه أن 3DNow متوقفة ولم يكتب لها النجاح، لكن معظم معالجات أنتل و AMD لازالت تدعم MMX.

بعدها بسنة، عام 1999، أطلقت أنتل الإضافة الجديدة SSE، حيث حلت مشاكل MMX عن طريق فصل المسجلات واستخدام تعليمات ومسجلات جديدة بطول 128 بت أسمتها XMM0 إلى XMM7، وكذلك دعمت أعداد الفاصلة العائمة، في البداية كانت تدعم إجراء حسابات على أربع أعداد float بطول 32 بت، لكن أضيفت أنواع أخرى وتعليمات جديدة عندما صدرت SSE2 عام 2001 مع معالج Pentium 4، حيث دعمت إجراء الحسابات على:

  • عددين double بطول 64 بت.
  • عددين صحيحين بطول 64 بت.
  • أربعة أعداد float بطول 32 بت (من SSE).
  • أربعة أعداد صحيحة بطول 32 بت.
  • ثمانية أعداد صحيحة بطول 16 بت.
  • ستة عشر عدداً صحيح بطول 8 بت.

أضيفت 8 مسجلات جديدة من XMM8 إلى XMM15 لتصبح 16 في معالجات x86-64، وأضافت SSE3 التي أتت مع معالجات Celeron وPentium Dual-Core وغيرها تعليمات أخرى تسمح لك بإجراء الحسابات أفقياً على نفس المسجل بدل إجراءها عمودياً فقط في النسخ السابقة.

هناك أيضاً SSE4، حيث أضافت تعليمات جديدة، أهمها تعليمة لإجراء الضرب القياسي dot product للمتجهات، مفيدة جداً، وهناك معمارية جديدة صدرت اسمها Advanced Vector Extensions، أو اختصاراً AVX، حيث أضافت تعليمات ومسجلات جديدة بطول 256 بت من YMM15-YMM0 (من YMM15-YMM8 تعمل في وضع 64 بت فقط)، وكذلك أضافت AVX2 تعليمات جديدة، المعماريات الأخيرة لاتدعمها كل المعالجات، تحتاج معالج حديث مثل i3 على الأقل، وهناك معماريات متقدمة مثل AVX-512، صدرت حديثاً عام 2015، والتي أضافت 32 مسجل بطول 512 بت، تدعمها المعالجات الحديثة من عائلة Skylake أو المعالجات الخاصة بالخوادم Xeon.

دعم المعالج لـSSE

يمكنك التحقق من دعم معالجك لـSSE باستخدام الأداة CPU-Z، حيث تعطيك معلومات عن معالجك والإضافات التي يدعمها، ادخل على صفحة الأداة، وفي أعلى يسار الصفحة ستجد خيار التنزيل، حمل الأداة، يفضل اختيار الملف المضغوط ZIP، ستجد برنامجين cpuz_xXX.exe أحدهما لأنظمة 32-بت والآخر لـ63-بت، شغل الأداة وستعرض لك نافذة كما في الصورة، المربع الأحمر عند Instructions يشير للإضافات التي يدعمها معالجك.

CPU-Z

يمكنك استخدام التعليمة CPUID إذا رغبت بمعرفة بيانات المعالج باستخدام برنامجك، حيث تسمح لك بالإستعلام عن المعالج والتعليمات التي يدعمها.

مرجع لتعلم SSE

غالباً لن تحتاج لكتابة SSE يدوياً كما أشرت، ويكفي أن تخبر المترجم بأن يستخدمها، لكن ستجد في هذا المرجع من أنتل قائمة بتعليمات SSE وAVX وغيرها معروضة بطريقة جميلة.


  1. تمثل الأعداد العشرية في المعالج باستخدام الفاصلة العائمة floating-point، تعتمد معظم المعالجات صيغة قياسية اسمها IEEE 754، أهم صيغتين موصوفة في IEEE 754 هما الـsingle precision بطول 32 بت وتكافئ float في C، والـdouble precision بطول 64 بت وتكافئ double، يمكنك البحث عن IEEE 754 للفهم آلية تمثيل الأعداد العشرية، لكنك لست بحاجة لفهمها كي تستخدم SSE.