Вопрос

Я выполняю рассеянное чтение 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.

Я делаю что-то не так?Разве это не тот код, который вы используете?Чего мне не хватает?Это только для меня?

Использование встроенных функций нарушает оптимизацию компилятора!

Весь смысл встроенных функций заключается в том, чтобы вставлять коды операций, о которых компилятор не знает, в поток кодов операций, о которых компилятор знает и сгенерировал.Если компилятору не предоставлены некоторые метаданные о коде операции и о том, как он влияет на регистры и память, компилятор не может предположить, что какие-либо данные сохраняются после выполнения встроенной функции.Это действительно вредит оптимизирующей части компилятора - он не может изменить порядок следования инструкций вокруг встроенных, он не может предположить, что регистры не затронуты и так далее.

Я думаю, что лучший способ оптимизировать это - посмотреть на картину в целом - вам нужно рассмотреть весь процесс от чтения исходных данных до записи конечного результата.Микрооптимизация редко дает большие результаты, если только вы не делаете что-то действительно плохое с самого начала.

Возможно, если вы подробно опишете требуемые входные и выходные данные, кто-нибудь здесь мог бы предложить оптимальный метод для их обработки.

Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top