Странность внутренней оптимизации 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 старомодным способом (со сдвигами и редакторами), а затем поместить их в __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 из-за того, что я предоставляю данные слишком быстро, но тогда я хотел бы получать точно такие же результаты, что бы я ни делал.Возможно ли, что первые 2 метода означают, что 16 нагрузок не могут быть распределены по циклу, чтобы скрыть задержку?Если да, то почему это происходит?Конечно , встроенная функция позволяет компилятору выполнять оптимизацию так , как и где ему заблагорассудится ..я думал , в этом весь смысл ...Также , несомненно , выполнение 16 операций чтения и 16 записей будет намного медленнее , чем 16 операций чтения и 1 запись с кучей инструкций по жонглированию SSE ...В конце концов, именно чтение и запись являются самыми медленными!
Любой, у кого есть какие-либо идеи о том, что происходит, будет высоко оценен!:D
Редактировать:В дополнение к комментарию ниже я прекратил предварительную загрузку байтов в качестве констант и изменил их на это:
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 секунды.Это не так хорошо , как прямая реализация на языке Си ...
Отредактируйте Еще раз:Лучшая производительность, которую я получаю на данный момент, это
// 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 ...Я все еще сбит с толку этим , но ..хо-хум.
Решение
Возможно, компилятор пытается поместить все аргументы встроенного в регистры сразу.Вы не хотите получать доступ к такому количеству переменных одновременно, не упорядочивая их.
Вместо того чтобы объявлять отдельный идентификатор для каждого образца, попробуйте поместить их в char[16]
.Компилятор будет добавлять 16 значений в регистры по своему усмотрению, при условии, что вы не будете использовать адрес чего-либо внутри массива.Вы можете добавить __aligned__
тег (или что бы там ни использовал VC ++) и, возможно, вообще избежать встроенного.В противном случае, вызывая внутреннюю с ( sample[15], sample[14], sample[13] … sample[0] )
должно облегчить работу компилятора или, по крайней мере, не навредить.
Редактировать: Я почти уверен, что вы боретесь с утечкой регистра, но это предложение, вероятно, просто сохранит байты по отдельности, а это не то, что вы хотите.Я думаю, мой совет состоит в том, чтобы чередовать вашу последнюю попытку (используя MakeFourCC) с операциями чтения, чтобы убедиться, что она запланирована правильно и без повторных обращений к стеку.Конечно, проверка объектного кода - лучший способ убедиться в этом.
По сути, вы передаете данные в файл register, а затем передаете их обратно.Вы же не хотите перегружать его до того, как придет время удалять данные.
Другие советы
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.
Я делаю что-то не так?Разве это не тот код, который вы используете?Чего мне не хватает?Это только для меня?
Использование встроенных функций нарушает оптимизацию компилятора!
Весь смысл встроенных функций заключается в том, чтобы вставлять коды операций, о которых компилятор не знает, в поток кодов операций, о которых компилятор знает и сгенерировал.Если компилятору не предоставлены некоторые метаданные о коде операции и о том, как он влияет на регистры и память, компилятор не может предположить, что какие-либо данные сохраняются после выполнения встроенной функции.Это действительно вредит оптимизирующей части компилятора - он не может изменить порядок следования инструкций вокруг встроенных, он не может предположить, что регистры не затронуты и так далее.
Я думаю, что лучший способ оптимизировать это - посмотреть на картину в целом - вам нужно рассмотреть весь процесс от чтения исходных данных до записи конечного результата.Микрооптимизация редко дает большие результаты, если только вы не делаете что-то действительно плохое с самого начала.
Возможно, если вы подробно опишете требуемые входные и выходные данные, кто-нибудь здесь мог бы предложить оптимальный метод для их обработки.