VC ++ SSE تحسين جوهري
-
18-09-2019 - |
سؤال
أقوم بإجراء قراءة متناثرة من بيانات 8 بت من ملف (إزالة ملف موجة 64 قناة). ثم أجمع بينهم ليكونوا مجرى بايت واحد. المشكلة التي أواجهها مع إعادة بناء البيانات الخاصة بي للكتابة.
في الأساس أنا أقرأ في 16 بايت ثم بناءها في متغير __m128i واحد ثم استخدام _mm_stream_ps لكتابة القيمة مرة أخرى إلى الذاكرة. ومع ذلك، لدي بعض نتائج الأداء الفردي.
في مخططي الأول، أستخدم جوائز _mm_set_epi8 لتعيين __m128i كما يلي:
const __m128i packedSamples = _mm_set_epi8( sample15, sample14, sample13, sample12, sample11, sample10, sample9, sample8,
sample7, sample6, sample5, sample4, sample3, sample2, sample1, sample0 );
في الأساس، أترك الأمر كله حتى التحويل البرمجي لتحديد كيفية تحسينه لإعطاء أفضل أداء. هذا يعطي أسوأ أداء. اختباري يعمل في ~ 0.195 ثانية.
ثانيا حاولت الاندماج باستخدام تعليمات 4 _mm_set_epi32 ثم تعبئتها
const __m128i samples0 = _mm_set_epi32( sample3, sample2, sample1, sample0 );
const __m128i samples1 = _mm_set_epi32( sample7, sample6, sample5, sample4 );
const __m128i samples2 = _mm_set_epi32( sample11, sample10, sample9, sample8 );
const __m128i samples3 = _mm_set_epi32( sample15, sample14, sample13, sample12 );
const __m128i packedSamples0 = _mm_packs_epi32( samples0, samples1 );
const __m128i packedSamples1 = _mm_packs_epi32( samples2, samples3 );
const __m128i packedSamples = _mm_packus_epi16( packedSamples0, packedSamples1 );
هذا يحسن الأداء إلى حد ما. يعمل الاختبار الخاص بي الآن في ~ 0.15 ثانية. يبدو مضادا بديهيا أن الأداء سيحسن من خلال القيام بذلك كما أفترض أن هذا هو بالضبط ما يفعله _mm_set_epi8 على أي حال ...
كانت محاولتي النهائية هي استخدام القليل من التعليمات البرمجية لدي من صنع أربع CCS طريقة الطراز القديم (مع التحولات أو ORS) ثم وضعها في __m128i باستخدام واحد _mm_set_epi32.
const GCui32 samples0 = MakeFourCC( sample0, sample1, sample2, sample3 );
const GCui32 samples1 = MakeFourCC( sample4, sample5, sample6, sample7 );
const GCui32 samples2 = MakeFourCC( sample8, sample9, sample10, sample11 );
const GCui32 samples3 = MakeFourCC( sample12, sample13, sample14, sample15 );
const __m128i packedSamples = _mm_set_epi32( samples3, samples2, samples1, samples0 );
هذا يعطي أداء أفضل. أخذ ~ 0.135 ثانية لتشغيل الاختبار الخاص بي. أنا حقا بدأت في الخلط.
لذلك حاولت نظام بايت كتابة بايت بايت بايت، وهذا هو أسرع قليلا بشكل طفيف من الطريقة الأخيرة.
ماذا يحدث؟ هذا كل شيء يبدو مضادا بديهية لي.
لقد فكرت في فكرة أن التأخير تحدث على _mm_stream_ps لأنني أرد بيانات بسرعة كبيرة ولكنني سأحصل على نفس النتائج التي أفعلها. هل من الممكن أن تعني الطرق الأولى الأولى أن الأحمال الستة عشر لا يمكن توزيعها من خلال الحلقة لإخفاء الكمون؟ إذا كان الأمر كذلك فلماذا هذا؟ بالتأكيد يسمح جوهري للمترجم بإجراء تحسينات كما وأين يرضي .. اعتقدت أن النقطة بأكملها ... بالتأكيد أداء 16 يقرأ و 16 يكتب سيكون أبطأ بكثير من 16 يقرأ و 1 اكتب مع مجموعة من شعوذة SSE تعليمات ... بعد كل القراءة والكتابة التي هي البتة البطيئة!
أي شخص لديه أي أفكار سوف يحدث موضع تقدير كبير! :د
تحرير: أبعد من التعليق أدناه توقفت عن تحميل البايتات على أنها ثوابت وتتغير إلى هذا:
const __m128i samples0 = _mm_set_epi32( *(pSamples + channelStep3), *(pSamples + channelStep2), *(pSamples + channelStep1), *(pSamples + channelStep0) );
pSamples += channelStep4;
const __m128i samples1 = _mm_set_epi32( *(pSamples + channelStep3), *(pSamples + channelStep2), *(pSamples + channelStep1), *(pSamples + channelStep0) );
pSamples += channelStep4;
const __m128i samples2 = _mm_set_epi32( *(pSamples + channelStep3), *(pSamples + channelStep2), *(pSamples + channelStep1), *(pSamples + channelStep0) );
pSamples += channelStep4;
const __m128i samples3 = _mm_set_epi32( *(pSamples + channelStep3), *(pSamples + channelStep2), *(pSamples + channelStep1), *(pSamples + channelStep0) );
pSamples += channelStep4;
const __m128i packedSamples0 = _mm_packs_epi32( samples0, samples1 );
const __m128i packedSamples1 = _mm_packs_epi32( samples2, samples3 );
const __m128i packedSamples = _mm_packus_epi16( packedSamples0, packedSamples1 );
وهذا الأداء المحسن إلى ~ 0.143 ثانية. Sitll ليس جيدا مثل التنفيذ C مستقيم ...
تحرير مرة أخرى: أفضل أداء أحصل عليه حتى الآن
// Load the samples.
const GCui8 sample0 = *(pSamples + channelStep0);
const GCui8 sample1 = *(pSamples + channelStep1);
const GCui8 sample2 = *(pSamples + channelStep2);
const GCui8 sample3 = *(pSamples + channelStep3);
const GCui32 samples0 = Build32( sample0, sample1, sample2, sample3 );
pSamples += channelStep4;
const GCui8 sample4 = *(pSamples + channelStep0);
const GCui8 sample5 = *(pSamples + channelStep1);
const GCui8 sample6 = *(pSamples + channelStep2);
const GCui8 sample7 = *(pSamples + channelStep3);
const GCui32 samples1 = Build32( sample4, sample5, sample6, sample7 );
pSamples += channelStep4;
// Load the samples.
const GCui8 sample8 = *(pSamples + channelStep0);
const GCui8 sample9 = *(pSamples + channelStep1);
const GCui8 sample10 = *(pSamples + channelStep2);
const GCui8 sample11 = *(pSamples + channelStep3);
const GCui32 samples2 = Build32( sample8, sample9, sample10, sample11 );
pSamples += channelStep4;
const GCui8 sample12 = *(pSamples + channelStep0);
const GCui8 sample13 = *(pSamples + channelStep1);
const GCui8 sample14 = *(pSamples + channelStep2);
const GCui8 sample15 = *(pSamples + channelStep3);
const GCui32 samples3 = Build32( sample12, sample13, sample14, sample15 );
pSamples += channelStep4;
const __m128i packedSamples = _mm_set_epi32( samples3, samples2, samples1, samples0 );
_mm_stream_ps( pWrite + 0, *(__m128*)&packedSamples );
هذا يعطيني المعالجة في ~ 0.095 ثانية أفضل بكثير. لا يبدو أنني قادر على الاقتران مع SSE على الرغم من ... ما زلت في حيرة من ذلك ولكن .. HO همهمة.
المحلول
ربما يحاول المترجم وضع جميع الحجج إلى جوهره في السجلات في وقت واحد. لا تريد الوصول إلى العديد من المتغيرات في وقت واحد دون تنظيمها.
بدلا من إعلان معرف منفصل لكل عينة، حاول وضعها في char[16]
. وبعد سيقوم المحول البرمجي بتعزيز القيم ال 16 للتسجيل حيث يرى مناسبا، طالما أنك لا تأخذ عنوان أي شيء داخل الصفيف. يمكنك إضافة __aligned__
العلامة (أو أيا كان استخدام VC ++) وربما تجنب جوهرها تماما. خلاف ذلك، استدعاء جوهري ( sample[15], sample[14], sample[13] … sample[0] )
يجب أن تجعل وظيفة التحويل البرمجي أسهل أو لا تقل ضرر.
يحرر: أنا متأكد من أنك تقاتل تسرب سجل ولكن هذا الاقتراح سيخزن فقط البايتات بشكل فردي، وهو ما لا تريده. أعتقد أن نصيحتي هي تدوير محاولتك النهائية (باستخدام Makefourcc) مع عمليات القراءة، للتأكد من أنه من المقرر بشكل صحيح ومع عدم وجود رحلات مستديرة إلى المكدس. بالطبع، تفتيش رمز الكائن هو أفضل طريقة للتأكد من ذلك.
أساسا، تقوم بتدفق البيانات في ملف التسجيل ثم بثته مرة أخرى. أنت لا تريد التحميل الزائد قبل حان الوقت لتدفئة البيانات.
نصائح أخرى
vs سيئة سيئة السمعة في تحسين جوهري. تتحرك بشكل خاص البيانات من وإلى سجلات SSE. يتم استخدام الجوهر نفسها بشكل جيد ولكن ....
ما تراه هو أنه يحاول ملء سجل SSE مع هذا الوحش:
00AA100C movzx ecx,byte ptr [esp+0Fh]
00AA1011 movzx edx,byte ptr [esp+0Fh]
00AA1016 movzx eax,byte ptr [esp+0Fh]
00AA101B movd xmm0,eax
00AA101F movzx eax,byte ptr [esp+0Fh]
00AA1024 movd xmm2,edx
00AA1028 movzx edx,byte ptr [esp+0Fh]
00AA102D movd xmm1,ecx
00AA1031 movzx ecx,byte ptr [esp+0Fh]
00AA1036 movd xmm4,ecx
00AA103A movzx ecx,byte ptr [esp+0Fh]
00AA103F movd xmm5,edx
00AA1043 movzx edx,byte ptr [esp+0Fh]
00AA1048 movd xmm3,eax
00AA104C movzx eax,byte ptr [esp+0Fh]
00AA1051 movdqa xmmword ptr [esp+60h],xmm0
00AA1057 movd xmm0,edx
00AA105B movzx edx,byte ptr [esp+0Fh]
00AA1060 movd xmm6,eax
00AA1064 movzx eax,byte ptr [esp+0Fh]
00AA1069 movd xmm7,ecx
00AA106D movzx ecx,byte ptr [esp+0Fh]
00AA1072 movdqa xmmword ptr [esp+20h],xmm4
00AA1078 movdqa xmmword ptr [esp+80h],xmm0
00AA1081 movd xmm4,ecx
00AA1085 movzx ecx,byte ptr [esp+0Fh]
00AA108A movdqa xmmword ptr [esp+70h],xmm2
00AA1090 movd xmm0,eax
00AA1094 movzx eax,byte ptr [esp+0Fh]
00AA1099 movdqa xmmword ptr [esp+10h],xmm4
00AA109F movdqa xmmword ptr [esp+50h],xmm6
00AA10A5 movd xmm2,edx
00AA10A9 movzx edx,byte ptr [esp+0Fh]
00AA10AE movd xmm4,eax
00AA10B2 movzx eax,byte ptr [esp+0Fh]
00AA10B7 movd xmm6,edx
00AA10BB punpcklbw xmm0,xmm1
00AA10BF punpcklbw xmm2,xmm3
00AA10C3 movdqa xmm3,xmmword ptr [esp+80h]
00AA10CC movdqa xmmword ptr [esp+40h],xmm4
00AA10D2 movd xmm4,ecx
00AA10D6 movdqa xmmword ptr [esp+30h],xmm6
00AA10DC movdqa xmm1,xmmword ptr [esp+30h]
00AA10E2 movd xmm6,eax
00AA10E6 punpcklbw xmm4,xmm5
00AA10EA punpcklbw xmm4,xmm0
00AA10EE movdqa xmm0,xmmword ptr [esp+50h]
00AA10F4 punpcklbw xmm1,xmm0
00AA10F8 movdqa xmm0,xmmword ptr [esp+70h]
00AA10FE punpcklbw xmm6,xmm7
00AA1102 punpcklbw xmm6,xmm2
00AA1106 movdqa xmm2,xmmword ptr [esp+10h]
00AA110C punpcklbw xmm2,xmm0
00AA1110 movdqa xmm0,xmmword ptr [esp+20h]
00AA1116 punpcklbw xmm1,xmm2
00AA111A movdqa xmm2,xmmword ptr [esp+40h]
00AA1120 punpcklbw xmm2,xmm0
00AA1124 movdqa xmm0,xmmword ptr [esp+60h]
00AA112A punpcklbw xmm3,xmm0
00AA112E punpcklbw xmm2,xmm3
00AA1132 punpcklbw xmm6,xmm4
00AA1136 punpcklbw xmm1,xmm2
00AA113A punpcklbw xmm6,xmm1
هذا يعمل بشكل أفضل و (يجب) أن تكون أسرع بشكل أسرع
__declspec(align(16)) BYTE arr[16] = { sample15, sample14, sample13, sample12, sample11, sample10, sample9, sample8, sample7, sample6, sample5, sample4, sample3, sample2, sample1, sample0 };
__m128i packedSamples = _mm_load_si128( (__m128i*)arr );
بناء سرير الاختبار الخاص بي:
void f()
{
const int steps = 1000000;
BYTE* pDest = new BYTE[steps*16+16];
pDest += 16 - ((ULONG_PTR)pDest % 16);
BYTE* pSrc = new BYTE[steps*16*16];
const int channelStep0 = 0;
const int channelStep1 = 1;
const int channelStep2 = 2;
const int channelStep3 = 3;
const int channelStep4 = 16;
__int64 freq;
QueryPerformanceFrequency( (LARGE_INTEGER*)&freq );
__int64 start = 0, end;
QueryPerformanceCounter( (LARGE_INTEGER*)&start );
for( int step = 0; step < steps; ++step )
{
__declspec(align(16)) BYTE arr[16];
for( int j = 0; j < 4; ++j )
{
//for( int i = 0; i < 4; ++i )
{
arr[0+j*4] = *(pSrc + channelStep0);
arr[1+j*4] = *(pSrc + channelStep1);
arr[2+j*4] = *(pSrc + channelStep2);
arr[3+j*4] = *(pSrc + channelStep3);
}
pSrc += channelStep4;
}
#if test1
// test 1 with C
for( int i = 0; i < 16; ++i )
{
*(pDest + step * 16 + i) = arr[i];
}
#else
// test 2 with SSE load/store
__m128i packedSamples = _mm_load_si128( (__m128i*)arr );
_mm_stream_si128( ((__m128i*)pDest) + step, packedSamples );
#endif
}
QueryPerformanceCounter( (LARGE_INTEGER*)&end );
printf( "%I64d", (end - start) * 1000 / freq );
}
بالنسبة لي اختبار 2 أسرع ثم اختبار 1.
هل أفعل شيئا خطأ؟ هل هذا ليس الرمز الذي تستخدمه؟ ماذا افتقد؟ هل هذا فقط بالنسبة لي؟
باستخدام تحسينات جوهرية مفيد التحويل البرمجي!
النقطة برمتها من الوظائف الجوهرية هي إدراج Opcodes لا يعرف المحول البرمجي في دفق Opcodes، يعرف المحول البرمجي وتوليد. ما لم يتم إعطاء برنامج التحويل البرمجي بعض بيانات التعريف حول OpCode وكيف يؤثر على السجلات والذاكرة، لا يمكن للمترجم افتراض أن يتم الحفاظ على أي بيانات بعد تنفيذ الجوهر. هذا يؤلمني حقا الجزء الأمثل من المحول البرمجي - لا يمكن إعادة ترتيب التعليمات حول الجوهرية، ولا يمكن أن تفترض أن السجلات تتأثر وما إلى ذلك.
أعتقد أن أفضل طريقة لتحسين هذا هو إلقاء نظرة على الصورة الأكبر - تحتاج إلى النظر في العملية برمتها من قراءة البيانات المصدر لكتابة الإخراج النهائي. نادرا ما تعطي تحسينات Micro نتائج كبيرة، إلا إذا كنت تفعل شيئا سيئا للغاية للبدء.
ربما، إذا قمت بالتفصيل الإدخال المطلوب والإخراج، فقد يقترح شخص ما هنا طريقة مثالية للتعامل معها.