代码之家  ›  专栏  ›  技术社区  ›  Goz

VC++SSE内在优化怪异

  •  2
  • Goz  · 技术社区  · 15 年前

    我正在从一个文件中执行8位数据的分散读取(取消64通道波形文件的交错读取)。然后我将它们组合成一个字节流。我面临的问题是,我要重新构建要写出的数据。

    基本上,我读取16个字节,然后将它们构建成一个单独的m128i变量,然后使用m m-stream-ps将值写回内存。不过,我有一些奇怪的性能结果。

    在我的第一个方案中,我使用mm_set_epi8内在设置我的uum128i如下:

        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个“设置”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秒。似乎有悖直觉的是,这样做会提高性能,因为我认为这正是集epi8所做的…

    我的最后一个尝试是使用一些我用传统方式制作四个ccs的代码(使用移位和ORS),然后用一个单独的EPI32将它们放入一个umm 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_u流上的想法,因为我提供数据的速度太快,但是无论做什么,我都会得到完全相同的结果。前2个方法是否意味着16个负载不能通过循环分布以隐藏延迟?如果是,为什么是这样?当然,一个内在的允许编译器根据自己的喜好进行优化。我以为这就是重点…当然,执行16次读和16次写的速度要比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秒。它不如直接的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……我还是很困惑,但是……嗬哼。

    3 回复  |  直到 13 年前
        1
  •  2
  •   Potatoswatter    15 年前

    也许编译器正试图同时将内部函数的所有参数放入寄存器。你不想一次访问那么多的变量而不组织它们。

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


    编辑: 我很确定你正在与寄存器溢出作斗争,但是这个建议可能只会单独存储字节,这不是你想要的。我认为我的建议是将您的最后一次尝试(使用makefourcc)与读取操作交织在一起,以确保它的计划正确,并且没有到堆栈的往返。当然,检查对象代码是确保这一点的最佳方法。

    实际上,您正在将数据流式传输到寄存器文件中,然后再将其流式传输出去。您不希望在数据刷新之前过载它。

        2
  •  2
  •   Christopher    15 年前

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

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

        3
  •  -3
  •   Skizz    15 年前

    使用intrinsics会破坏编译器优化!

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

    我认为优化这一点的最佳方法是从更大的角度来看——您需要考虑从读取源数据到编写最终输出的整个过程。微观优化很少能产生大的结果,除非你开始做的事情真的很糟糕。

    也许,如果您详细描述了所需的输入和输出,这里的某个人可以建议一种最佳的方法来处理它。