كيفية حساب منتج ناقل النقل باستخدام وظائف SSE الجوهرية في C.

StackOverflow https://stackoverflow.com/questions/4120681

سؤال

أحاول ضرب اثنين من المتجهات معًا حيث يتم ضرب كل عنصر من عناصر ناقل واحد بواسطة العنصر في نفس الفهرس في المتجه الآخر. ثم أريد أن يلخص جميع عناصر المتجه الناتج للحصول على رقم واحد. على سبيل المثال ، سيبدو الحساب مثل هذا المتجهات {1،2،3،4} و {5،6،7،8}:

1*5+2*6+3*7+4*8

في الأساس ، أنا آخذ منتج DOT للناقلين. أعلم أن هناك أمر SSE للقيام بذلك ، لكن الأمر ليس له وظيفة جوهرية مرتبطة به. في هذه المرحلة ، لا أريد أن أكتب التجميع المضمّن في رمز C الخاص بي ، لذلك أريد استخدام الوظائف الجوهرية فقط. هذا يبدو وكأنه حساب شائع لذلك أنا مندهش من نفسي أنني لم أتمكن من العثور على الإجابة على Google.

ملاحظة: أقوم بتحسين بنية صغيرة محددة تدعم ما يصل إلى SSE 4.2.

شكرا لمساعدتك.

هل كانت مفيدة؟

المحلول

إذا كنت تقوم بمنتج ناتج عن المتجهات الأطول ، فاستخدم مضاعفًا ومنتظمًا _mm_add_ps (أو FMA) داخل الحلقة الداخلية. احفظ المبلغ الأفقي حتى النهاية.


ولكن إذا كنت تقوم بمنتج DOT لمجرد ناقلات SIMD:

يتضمن GCC (على الأقل الإصدار 4.3) <smmintrin.h> مع المواد الجوهرية مستوى SSE4.1 ، بما في ذلك منتجات DOT الفردية والمزدوجة:

_mm_dp_ps (__m128 __X, __m128 __Y, const int __M);
_mm_dp_pd (__m128d __X, __m128d __Y, const int __M);

على وحدة المعالجة المركزية في Intel السائدة (وليس الذرة/SilverMont) ، هذه أسرع إلى حد ما من القيام بذلك يدويًا مع تعليمات متعددة.

ولكن على AMD (بما في ذلك Ryzen) ، dpps أبطأ بكثير. (نرى جداول تعليمات Agner Fog)


كإعداد للمعالجات الأكبر سناً ، يمكنك استخدام هذه الخوارزمية لإنشاء منتج DOT الخاص بالمتجهات a و b:

__m128 r1 = _mm_mul_ps(a, b);

ثم المبلغ الأفقي r1 استخدام أسرع طريقة للقيام بمجموعة متجه عائم أفقي على x86 (انظر هناك للحصول على نسخة معلقة من هذا ، ولماذا يكون أسرع.)

__m128 shuf   = _mm_shuffle_ps(r1, r1, _MM_SHUFFLE(2, 3, 0, 1));
__m128 sums   = _mm_add_ps(r1, shuf);
shuf          = _mm_movehl_ps(shuf, sums);
sums          = _mm_add_ss(sums, shuf);
float result =  _mm_cvtss_f32(sums);

بديل بطيء يكلف 2 - hadd, ، والتي من شأنها أن تخنث بسهولة على إنتاجية خلط ، وخاصة على وحدة المعالجة المركزية Intel.

r2 = _mm_hadd_ps(r1, r1);
r3 = _mm_hadd_ps(r2, r2);
_mm_store_ss(&result, r3);

نصائح أخرى

أود أن أقول أن أسرع طريقة SSE ستكون:

static inline float CalcDotProductSse(__m128 x, __m128 y) {
    __m128 mulRes, shufReg, sumsReg;
    mulRes = _mm_mul_ps(x, y);

    // Calculates the sum of SSE Register - https://stackoverflow.com/a/35270026/195787
    shufReg = _mm_movehdup_ps(mulRes);        // Broadcast elements 3,1 to 2,0
    sumsReg = _mm_add_ps(mulRes, shufReg);
    shufReg = _mm_movehl_ps(shufReg, sumsReg); // High Half -> Low Half
    sumsReg = _mm_add_ss(sumsReg, shufReg);
    return  _mm_cvtss_f32(sumsReg); // Result in the lower part of the SSE Register
}

تتبعت - أسرع طريقة للقيام بمجموعة متجه عائم أفقي على x86.

كتبت هذا وتجميعه مع gcc -O3 -S -ftree-vectorize -ftree-vectorizer-verbose=2 sse.c

void f(int * __restrict__ a, int * __restrict__ b, int * __restrict__ c, int * __restrict__ d,
       int * __restrict__ e, int * __restrict__ f, int * __restrict__ g, int * __restrict__ h,
       int * __restrict__ o)
{
    int i;

    for (i = 0; i < 8; ++i)
        o[i] = a[i]*e[i] + b[i]*f[i] + c[i]*g[i] + d[i]*h[i];
}

و GCC 4.3.0 تلقائيًا-

sse.c:5: note: LOOP VECTORIZED.
sse.c:2: note: vectorized 1 loops in function.

ومع ذلك ، فإنه لن يفعل ذلك إلا إذا استخدمت حلقة مع تكرارات كافية - وإلا فإن الإخراج المطول سيوضح أن التقييم غير مربحة أو كانت الحلقة صغيرة جدًا. بدون ال __restrict__ الكلمات الرئيسية يجب أن تنشئ إصدارات منفصلة وغير مستقلة للتعامل مع الحالات التي يكون فيها الإخراج o قد تشير إلى أحد المدخلات.

أود أن ألصق الإرشادات كمثال ، ولكن بما أن جزءًا من التقييم ، فكّت الحلقة ، فهي غير قابلة للقراءة للغاية.

هناك مقال بقلم إنتل هنا التي تلامس تطبيقات النقطة.

مرخصة بموجب: CC-BY-SA مع الإسناد
لا تنتمي إلى StackOverflow
scroll top