【问题标题】:packing 10 bit values into a byte stream with SIMD [duplicate]使用 SIMD 将 10 位值打包成一个字节流 [重复]
【发布时间】:2014-05-14 20:00:03
【问题描述】:

我正在尝试使用 SIMD 指令将 10 位像素打包成一个连续的字节流。下面的代码“原则上”执行此操作,但 SIMD 版本比标量版本慢。

问题似乎是我找不到可以有效加载寄存器的好的收集/分散操作。

有什么改进建议吗?

// SIMD_test.cpp : Defines the entry point for the console application.
//

#include "stdafx.h"

#include "Windows.h"
#include <tmmintrin.h>
#include <stdint.h>
#include <string.h>

// reference non-SIMD implementation that "works"
// 4 uint16 at a time as input, and 5 uint8 as output per loop iteration

void packSlow(uint16_t* ptr, uint8_t* streamBuffer, uint32_t NCOL)
{
    for(uint32_t j=0;j<NCOL;j+=4)
    {
        streamBuffer[0] = (uint8_t)(ptr[0]);
        streamBuffer[1] = (uint8_t)(((ptr[0]&0x3FF)>>8) | ((ptr[1]&0x3F) <<2));
        streamBuffer[2] = (uint8_t)(((ptr[1]&0x3FF)>>6) | ((ptr[2]&0x0F) <<4));
        streamBuffer[3] = (uint8_t)(((ptr[2]&0x3FF)>>4) | ((ptr[3]&0x03) <<6));
        streamBuffer[4] = (uint8_t)((ptr[3]&0x3FF)>>2) ;
        streamBuffer += 5;
        ptr += 4;
    }
}


// poorly written SIMD implementation. Attempts to do the same
// as the packSlow, but 8 iterations at a time

void packFast(uint16_t* ptr, uint8_t* streamBuffer, uint32_t NCOL)
{
    const __m128i maska = _mm_set_epi16(0x3FF,0x3FF,0x3FF,0x3FF,0x3FF,0x3FF,0x3FF,0x3FF);
    const __m128i maskb = _mm_set_epi16(0x3F,0x3F,0x3F,0x3F,0x3F,0x3F,0x3F,0x3F);
    const __m128i maskc = _mm_set_epi16(0x0F,0x0F,0x0F,0x0F,0x0F,0x0F,0x0F,0x0F);
    const __m128i maskd = _mm_set_epi16(0x03,0x03,0x03,0x03,0x03,0x03,0x03,0x03);

    for(uint32_t j=0;j<NCOL;j+=4*8)
    {
        _mm_prefetch((const char*)(ptr+j),_MM_HINT_T0);
    }

    for(uint32_t j=0;j<NCOL;j+=4*8)
    {
        // this "fetch" stage is costly. Each term takes 2 cycles
        __m128i ptr0 = _mm_set_epi16(ptr[0],ptr[4],ptr[8],ptr[12],ptr[16],ptr[20],ptr[24],ptr[28]);
        __m128i ptr1 = _mm_set_epi16(ptr[1],ptr[5],ptr[9],ptr[13],ptr[17],ptr[21],ptr[25],ptr[29]);
        __m128i ptr2 = _mm_set_epi16(ptr[2],ptr[6],ptr[10],ptr[14],ptr[18],ptr[22],ptr[26],ptr[30]);
        __m128i ptr3 = _mm_set_epi16(ptr[3],ptr[7],ptr[11],ptr[15],ptr[19],ptr[23],ptr[27],ptr[31]);

           // I think this part is fairly well optimized
        __m128i streamBuffer0 =  ptr0;
        __m128i streamBuffer1 = _mm_or_si128(_mm_srl_epi16 (_mm_and_si128 (ptr0 , maska), _mm_set_epi32(0, 0, 0,8)) , _mm_sll_epi16 (_mm_and_si128 (ptr1 , maskb) , _mm_set_epi32(0, 0, 0,2)));
        __m128i streamBuffer2 = _mm_or_si128(_mm_srl_epi16 (_mm_and_si128 (ptr1 , maska), _mm_set_epi32(0, 0, 0,6)) , _mm_sll_epi16 (_mm_and_si128 (ptr2 , maskc) , _mm_set_epi32(0, 0, 0,4)));
        __m128i streamBuffer3 = _mm_or_si128(_mm_srl_epi16 (_mm_and_si128 (ptr2 , maska), _mm_set_epi32(0, 0, 0,4)) , _mm_sll_epi16 (_mm_and_si128 (ptr3 , maskd) , _mm_set_epi32(0, 0, 0,6)));
        __m128i streamBuffer4 = _mm_srl_epi16 (_mm_and_si128 (ptr3 , maska), _mm_set_epi32(0, 0, 0,2)) ;

        // this again is terribly slow. ~2 cycles per byte output
        for(int j=15;j>=0;j-=2)
        {
            streamBuffer[0] = streamBuffer0.m128i_u8[j];
            streamBuffer[1] = streamBuffer1.m128i_u8[j];
            streamBuffer[2] = streamBuffer2.m128i_u8[j];
            streamBuffer[3] = streamBuffer3.m128i_u8[j];
            streamBuffer[4] = streamBuffer4.m128i_u8[j];
            streamBuffer += 5;
        }
        ptr += 32;
    }

}

int _tmain(int argc, _TCHAR* argv[])
{

    uint16_t pixels[512];
    uint8_t packed1[512*10/8];
    uint8_t packed2[512*10/8];

    for(int i=0;i<512;i++)
    {
        pixels[i] = i;
    }

    LARGE_INTEGER t0,t1,t2;

    QueryPerformanceCounter(&t0);
    for(int k=0;k<1000;k++) packSlow(pixels,packed1,512);
    QueryPerformanceCounter(&t1);
    for(int k=0;k<1000;k++) packFast(pixels,packed2,512);
    QueryPerformanceCounter(&t2);

    printf("%d %d\n",t1.QuadPart-t0.QuadPart,t2.QuadPart-t1.QuadPart);

    if (memcmp(packed1,packed2,sizeof(packed1)))
    {
        printf("failed\n");
    }


    return 0;
}

【问题讨论】:

  • 这似乎更像是一个代码审查问题codereview.stackexchange.com
  • 是的,这将非常糟糕。您需要保留整个循环 SIMD,而不仅仅是其中的一小部分。
  • @CoryNelson - 是的,它很慢。您对如何将非 SIMD 部分重写为 SIMD 有什么建议吗?有没有一种简单的方法可以使用 SSE 指令有效地收集/分散负载?
  • 如果在我下班回家时没有人回答,我可能会试一试。这有很大的改进空间。一般建议:避免使用_mm_set_epi16 加载,并且不要访问__m128i 成员。

标签: c++ x86 bit-manipulation simd


【解决方案1】:

在重新阅读您的代码时,看起来您几乎肯定是在谋杀您的加载/存储单元,新的 AVX2 VGATHER[D/Q]P[D/S] 指令系列甚至无法完全缓解。甚至 Haswell 的架构仍然需要每个负载元素一个 uop,每个负载元素都命中 L1D TLB 和缓存,无论位置如何,在 Skylake ca 中显示效率提高。最早是 2016 年。

您目前最好的办法可能是进行 16B 寄存器读取并使用寄存器副本、_mm_shuffle_epi8()_mm_or_si128() 调用手动构造您的 streamBuffer 值,而精加工存储则相反。强>

在不久的将来,AVX2 将提供(并且已经为较新的桌面提供)VPS[LL/RL/RA]V[D/Q] 指令,允许可变元素移动,结合水平添加,可以非常快速地完成此打包。在这种情况下,您可以使用简单的 MOVDQU 指令来加载您的值,因为您可以在单个 xmm 寄存器中处理连续的 uint16_t 输入值。

另外,请考虑重新设计您的预取。 NCOL 循环中的 j 一次处理 64B/1 个缓存行,因此您可能应该在第二个循环主体的开头为 ptr + 32 进行一次预取。您甚至可以考虑省略它,因为它是一个简单的前向扫描,硬件预取器会在极少数迭代后为您检测并自动执行。

【讨论】:

    【解决方案2】:

    我没有专门的 SSE 经验。但我会尝试如下优化代码。

    // warning. This routine requires streamBuffer to have at least 3 extra spare bytes
    // at the end to be used as scratch space. It will write 0's to those bytes.
    // for example, streamBuffer needs to be 640+3 bytes of allocated memory if
    // 512 10-bit samples are output.
    
    void packSlow1(uint16_t* ptr, uint8_t* streamBuffer, uint32_t NCOL)
    {
        for(uint32_t j=0;j<NCOL;j+=4*4)
        {
            uint64_t *dst;
            uint64_t src[4][4];
    
            // __m128i s01 = _mm_set_epi64(ptr[0], ptr[1]);
            // __m128i s23 = _mm_set_epi64(ptr[2], ptr[3]);
            // ---- or ----
            // __m128i s0123 = _mm_load_si128(ptr[0])
            // __m128i s01   = _?????_(s0123) // some instruction to extract s01 from s0123
            // __m128i s23   = _?????_(s0123) // some instruction to extract s23
    
            src[0][0] = ptr[0] & 0x3ff;
            src[0][1] = ptr[1] & 0x3ff;
            src[0][2] = ptr[2] & 0x3ff;
            src[0][3] = ptr[3] & 0x3ff;
    
            src[1][0] = ptr[4] & 0x3ff;
            src[1][1] = ptr[5] & 0x3ff;
            src[1][2] = ptr[6] & 0x3ff;
            src[1][3] = ptr[7] & 0x3ff;
    
            src[2][0] = ptr[8] & 0x3ff;
            src[2][1] = ptr[9] & 0x3ff;
            src[2][2] = ptr[10] & 0x3ff;
            src[2][3] = ptr[11] & 0x3ff;
    
            src[3][0] = ptr[12] & 0x3ff;
            src[3][1] = ptr[13] & 0x3ff;
            src[3][2] = ptr[14] & 0x3ff;
            src[3][3] = ptr[15] & 0x3ff;
    
            // looks like _mm_maskmoveu_si128 can store result efficiently
            dst = (uint64_t*)streamBuffer;
            dst[0] = src[0][0] | (src[0][1] << 10) | (src[0][2] << 20) | (src[0][3] << 30);
    
            dst = (uint64_t*)(streamBuffer + 5);
            dst[0] = src[1][0] | (src[1][1] << 10) | (src[1][2] << 20) | (src[1][3] << 30);
    
            dst = (uint64_t*)(streamBuffer + 10);
            dst[0] = src[2][0] | (src[2][1] << 10) | (src[2][2] << 20) | (src[2][3] << 30);
    
            dst = (uint64_t*)(streamBuffer + 15);
            dst[0] = src[3][0] | (src[3][1] << 10) | (src[3][2] << 20) | (src[3][3] << 30);
    
            streamBuffer += 5 * 4;
            ptr += 4 * 4;
        }
    }
    

    更新:

    基准测试:

    Ubuntu 12.04, x86_64 GNU/Linux, gcc v4.6.3 (Virtual Box)
    Intel Core i7 (Macbook pro)
    compiled with -O3
    
    5717633386 (1X):   packSlow
    3868744491 (1.4X): packSlow1 (version from the post)
    4471858853 (1.2X): packFast2 (from Mark Lakata's post)
    1820784764 (3.1X): packFast3 (version from the post)
    
    Windows 8.1, x64, VS2012 Express
    Intel Core i5 (Asus)
    compiled with standard 'Release' options and SSE2 enabled
    
    00413185 (1X)   packSlow
    00782005 (0.5X) packSlow1
    00236639 (1.7X) packFast2
    00148906 (2.8X) packFast3
    

    我在装有 Windows 8.1 和 VS Express 2012 的华硕笔记本上看到完全不同的结果(使用 -O2 编译的代码)。 packSlow1 比原始 packSlow 慢 2 倍,而 packFast2 比 packSlow 快 1.7 倍(不是 2.9 倍)。研究了这个问题后,我明白了原因。 VC 编译器无法将所有常量保存到 packFast2 的 XMMS 寄存器中,因此它在循环中插入了额外的内存访问(参见生成的程序集)。缓慢的内存访问解释了性能下降。

    为了获得更稳定的结果,我将像素缓冲区增加到 256x512,并将循环计数器从 1000 增加到 10000000/256。

    这是我的 SSE 优化功能版本。

    // warning. This routine requires streamBuffer to have at least 3 extra spare bytes
    // at the end to be used as scratch space. It will write 0's to those bytes.
    // for example, streamBuffer needs to be 640+3 bytes of allocated memory if
    // 512 10-bit samples are output.
    
    void packFast3(uint16_t* ptr, uint8_t* streamBuffer, uint32_t NCOL)
    {
        const __m128i m0 = _mm_set_epi16(0, 0x3FF, 0, 0x3FF, 0, 0x3FF, 0, 0x3FF);
        const __m128i m1 = _mm_set_epi16(0x3FF, 0, 0x3FF, 0, 0x3FF, 0, 0x3FF, 0);
        const __m128i m2 = _mm_set_epi32(0, 0xFFFFFFFF, 0, 0xFFFFFFFF);
        const __m128i m3 = _mm_set_epi32(0xFFFFFFFF, 0, 0xFFFFFFFF, 0);
        const __m128i m4 = _mm_set_epi32(0, 0, 0xFFFFFFFF, 0xFFFFFFFF);
        const __m128i m5 = _mm_set_epi32(0xFFFFFFFF, 0xFFFFFFFF, 0, 0);
        __m128i s0, t0, r0, x0, x1;
    
        // unrolled and normal loop gives the same result
        for(uint32_t j=0;j<NCOL;j+=8)
        {
            // load 8 samples into s0
            s0 = _mm_loadu_si128((__m128i*)ptr);            // s0=00070006_00050004_00030002_00010000
    
            // join 16-bit samples into 32-bit words
            x0 = _mm_and_si128(s0, m0);                     // x0=00000006_00000004_00000002_00000000
            x1 = _mm_and_si128(s0, m1);                     // x1=00070000_00050000_00030000_00010000
            t0 = _mm_or_si128(x0, _mm_srli_epi32(x1, 6));   // t0=00001c06_00001404_00000c02_00000400
    
            // join 32-bit words into 64-bit dwords
            x0 = _mm_and_si128(t0, m2);                     // x0=00000000_00001404_00000000_00000400
            x1 = _mm_and_si128(t0, m3);                     // x1=00001c06_00000000_00000c02_00000000
            t0 = _mm_or_si128(x0, _mm_srli_epi64(x1, 12));  // t0=00000001_c0601404_00000000_c0200400
    
            // join 64-bit dwords
            x0 = _mm_and_si128(t0, m4);                     // x0=00000000_00000000_00000000_c0200400
            x1 = _mm_and_si128(t0, m5);                     // x1=00000001_c0601404_00000000_00000000
            r0 = _mm_or_si128(x0, _mm_srli_si128(x1, 3));   // r0=00000000_000001c0_60140400_c0200400
    
            // and store result
            _mm_storeu_si128((__m128i*)streamBuffer, r0);
    
            streamBuffer += 10;
            ptr += 8;
        }
    }
    

    【讨论】:

    • 谢谢。这比我的幼稚代码运行得快得多,275 个滴答声与 751 个(快 2.7 倍)相比。
    • 除非有“错误”或意外的副作用。将最后一个dst[0] 写入streamBuffer+15 时出现缓冲区溢出。你只想写 5 个字节(40 位),但你写了 8 个字节,所以最后 3 个字节覆盖了堆栈的一部分,在我的例子中,破坏了一个不同的变量。我想解决方法是确保在 streamBuffer 的末尾至少有 3 个字节的“填充”。
    • @Mark Lakata,我认为如果使用 SIMD 重写代码,它的运行速度甚至可以提高 2 倍。我会尝试使用它。
    • 我不确定发生了什么变化,但由于某种原因,今天的速度提升有所不同。 368 滴答对 213 滴答(快 1.7 倍)。我确实在 SIMD 方面有所改进,尽管没有我希望的那么多。
    • 在您的 cmets 中,您提到 _mm_maskmoveu_si128 可以有效使用,但我发现 _mm_storeu_si128 明显更快。在这种情况下,我正在做和你一样的技巧,使用 64 位寄存器写入 40 位,除了我使用 128 位寄存器写入 80 位,所以最后 58 位不断被覆盖,这没关系,除了输出缓冲区必须有额外的 8 个字节以避免溢出。
    【解决方案3】:

    我使用 SIMD 提出了一个“更好”的解决方案,但它没有利用并行化,只是更高效的加载和存储(我认为)。

    我在这里发布以供参考,不一定是最佳答案。

    基准是(任意滴答)

     gcc4.8.1 -O3    VS2012 /O2      Implementation
     ----------------------------------------- 
     369 (1X)        3394 (1X)       packSlow (original code)
     212 (1.7X)      2010 (1.7X)     packSlow (from @alexander)
     147 (2.5X)      1178 (2.9X)     packFast2 (below)
    

    这是代码。本质上是@alexander 的代码,除了使用 128 位寄存器而不是 64 位寄存器,展开 2x 而不是 4x。

    void packFast2(uint16_t* ptr, uint8_t* streamBuffer, uint32_t NCOL)
    {
        const __m128i maska = _mm_set_epi16(0x3FF,0x3FF,0x3FF,0x3FF,0x3FF,0x3FF,0x3FF,0x3FF);
        const __m128i mask0 = _mm_set_epi16(0,0,0,0,0,0,0,0x3FF);
        const __m128i mask1 = _mm_set_epi16(0,0,0,0,0,0,0x3FF,0);
        const __m128i mask2 = _mm_set_epi16(0,0,0,0,0,0x3FF,0,0);
        const __m128i mask3 = _mm_set_epi16(0,0,0,0,0x3FF,0,0,0);
        const __m128i mask4 = _mm_set_epi16(0,0,0,0x3FF,0,0,0,0);
        const __m128i mask5 = _mm_set_epi16(0,0,0x3FF,0,0,0,0,0);
        const __m128i mask6 = _mm_set_epi16(0,0x3FF,0,0,0,0,0,0);
        const __m128i mask7 = _mm_set_epi16(0x3FF,0,0,0,0,0,0,0);
    
        for(uint32_t j=0;j<NCOL;j+=16)
        {
            __m128i s = _mm_load_si128((__m128i*)ptr); // load 8 16 bit values
            __m128i s2 = _mm_load_si128((__m128i*)(ptr+8)); // load 8 16 bit values
    
            __m128i a = _mm_and_si128(s,mask0);
            a = _mm_or_si128( a, _mm_srli_epi64 (_mm_and_si128(s, mask1),6));
            a = _mm_or_si128( a, _mm_srli_epi64 (_mm_and_si128(s, mask2),12));
            a = _mm_or_si128( a, _mm_srli_epi64 (_mm_and_si128(s, mask3),18));
            a = _mm_or_si128( a, _mm_srli_si128 (_mm_and_si128(s, mask4),24/8)); // special shift 24 bits to the right, staddling the middle. luckily use just on 128 byte shift (24/8)
            a = _mm_or_si128( a, _mm_srli_si128 (_mm_srli_epi64 (_mm_and_si128(s, mask5),6),24/8)); // special. shift net 30 bits. first shift 6 bits, then 3 bytes.
            a = _mm_or_si128( a, _mm_srli_si128 (_mm_srli_epi64 (_mm_and_si128(s, mask6),4),32/8)); // special. shift net 36 bits. first shift 4 bits, then 4 bytes (32 bits).
            a = _mm_or_si128( a, _mm_srli_epi64 (_mm_and_si128(s, mask7),42));
    
            _mm_storeu_si128((__m128i*)streamBuffer, a);
    
            __m128i a2 = _mm_and_si128(s2,mask0);
            a2 = _mm_or_si128( a2, _mm_srli_epi64 (_mm_and_si128(s2, mask1),6));
            a2 = _mm_or_si128( a2, _mm_srli_epi64 (_mm_and_si128(s2, mask2),12));
            a2 = _mm_or_si128( a2, _mm_srli_epi64 (_mm_and_si128(s2, mask3),18));
            a2 = _mm_or_si128( a2, _mm_srli_si128 (_mm_and_si128(s2, mask4),24/8)); // special shift 24 bits to the right, staddling the middle. luckily use just on 128 byte shift (24/8)
            a2 = _mm_or_si128( a2, _mm_srli_si128 (_mm_srli_epi64 (_mm_and_si128(s2, mask5),6),24/8)); // special. shift net 30 bits. first shift 6 bits, then 3 bytes.
            a2 = _mm_or_si128( a2, _mm_srli_si128 (_mm_srli_epi64 (_mm_and_si128(s2, mask6),4),32/8)); // special. shift net 36 bits. first shift 4 bits, then 4 bytes (32 bits).
            a2 = _mm_or_si128( a2, _mm_srli_epi64 (_mm_and_si128(s2, mask7),42));
    
            _mm_storeu_si128((__m128i*)(streamBuffer+10), a2);
    
            streamBuffer += 20 ;
            ptr += 16 ;
        }
    }
    

    【讨论】:

    • 顺便说一句,_mm_srli_si128bytes 而不是 bits 移动,这很烦人。这解释了我对 mask5、mask6 和 mask 7 的愚蠢
    猜你喜欢
    • 2013-09-09
    • 2013-01-07
    • 2016-04-11
    • 2011-06-16
    • 2014-12-19
    • 1970-01-01
    • 2011-07-19
    • 1970-01-01
    • 2018-04-30
    相关资源
    最近更新 更多