2

我正在从文件中执行 8 位数据的分散读取(De-Interleaving a 64 channel wave file)。然后我将它们组合成一个字节流。我遇到的问题是我重新构建要写出的数据。

基本上我正在读取 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 正在做的事情......

我最后的尝试是使用一些代码,我用老式的方式制作四个 CC(使用 shift 和 ors),然后使用单个 _mm_set_epi32 将它们放入 __m128i 中。

    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 次写入肯定会比使用一堆 SSE 杂耍的 16 次读取和 1 次写入慢得多说明......毕竟它的读写速度很慢!

任何有任何想法的人都将不胜感激!: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 秒。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 接近……我仍然对此感到困惑,但是……哼哼。

4

3 回答 3

2

也许编译器正试图一次将所有内部参数放入寄存器中。您不想在不组织它们的情况下一次访问那么多变量。

不要为每个样本声明一个单独的标识符,而是尝试将它们放入char[16]. 只要您不获取数组中任何内容的地址,编译器就会将这 16 个值提升到它认为合适的寄存器中。您可以添加一个__aligned__标签(或任何 VC++ 使用的标签),并且可能完全避免使用内在标签。否则,调用内部函数( sample[15], sample[14], sample[13] … sample[0] )应该会使编译器的工作更容易,或者至少不会造成伤害。


编辑:我很确定您正在与寄存器溢出作斗争,但该建议可能只会单独存储字节,这不是您想要的。我认为我的建议是将您的最终尝试(使用 MakeFourCC)与读取操作交错,以确保正确安排它并且没有往返堆栈。当然,检查目标代码是确保这一点的最佳方式。

本质上,您是将数据流式传输到寄存器文件中,然后将其流式传输回来。在刷新数据之前,您不希望重载它。

于 2010-01-05T10:19:24.953 回答
2

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 更快。

我做错了吗?这不是您正在使用的代码吗?我想念什么?这只是给我的吗?

于 2010-01-05T12:46:36.707 回答
-3

使用内在函数会破坏编译器优化!

内在函数的重点是将编译器不知道的操作码插入到编译器知道并生成的操作码流中。除非给编译器一些关于操作码的元数据以及它如何影响寄存器和内存,否则编译器不能假设在执行内在函数后保留了任何数据。这确实伤害了编译器的优化部分——它不能围绕内在函数重新排序指令,它不能假设寄存器不受影响等等。

我认为优化这一点的最佳方法是着眼于大局——您需要考虑从读取源数据到写入最终输出的整个过程。微观优化很少会产生大的结果,除非你一开始就做的很糟糕。

也许,如果您详细说明所需的输入和输出,这里的某人可能会建议一种最佳方法来处理它。

于 2010-01-05T11:30:56.577 回答