使用SSE最快缩小8位灰度图像

时间:2017-08-07 07:31:09

标签: c++ image-processing x86 sse simd

我有一个功能,可以将8位图像缩小两倍。我之前有optimised the rgb32 case with SSE。现在我想对gray8案例做同样的事情。

在核心,有一个函数采用两行像素数据,其工作方式如下:

/** 
 * Calculates the average of two rows of gray8 pixels by averaging four pixels.
 */
void average2Rows(const uint8_t* row1, const uint8_t* row2, uint8_t* dst, int size)
{
    for (int i = 0; i < size - 1; i += 2)
        *(dst++) = ((row1[i]+row1[i+1]+row2[i]+row2[i+1])/4)&0xFF;
}

现在,我已经提出了一个SSE变体,其速度提高了大约三倍,但它确实涉及大量改组,我认为可能会做得更好。有人看到这里可以优化的内容吗?

/* row1: 16 8-bit values A-P
 * row2: 16 8-bit values a-p
 * returns 16 8-bit values (A+B+a+b)/4, (C+D+c+d)/4, ..., (O+P+o+p)/4
 */
__m128i avg16Bytes(const __m128i& row1, const __m128i& row2)
{
    static const __m128i  zero = _mm_setzero_si128(); 

    __m128i ABCDEFGHIJKLMNOP = _mm_avg_epu8(row1_u8, row2);

    __m128i ABCDEFGH  = _mm_unpacklo_epi8(ABCDEFGHIJKLMNOP, zero);
    __m128i IJKLMNOP  = _mm_unpackhi_epi8(ABCDEFGHIJKLMNOP, zero);

    __m128i AIBJCKDL = _mm_unpacklo_epi16( ABCDEFGH, IJKLMNOP );
    __m128i EMFNGOHP = _mm_unpackhi_epi16( ABCDEFGH, IJKLMNOP );

    __m128i AEIMBFJN = _mm_unpacklo_epi16( AIBJCKDL, EMFNGOHP );
    __m128i CGKODHLP = _mm_unpackhi_epi16( AIBJCKDL, EMFNGOHP );

    __m128i ACEGIKMO = _mm_unpacklo_epi16( AEIMBFJN, CGKODHLP );
    __m128i BDFHJLNP = _mm_unpackhi_epi16( AEIMBFJN, CGKODHLP );

    return _mm_avg_epu8(ACEGIKMO, BDFHJLNP);
}

/*
 * Calculates the average of two rows of gray8 pixels by averaging four pixels.
 */
void average2Rows(const uint8_t* src1, const uint8_t* src2, uint8_t* dst, int size)
{
    for(int i = 0;i<size-31; i+=32)
    {
        __m128i tl = _mm_loadu_si128((__m128i const*)(src1+i));
        __m128i tr = _mm_loadu_si128((__m128i const*)(src1+i+16));
        __m128i bl = _mm_loadu_si128((__m128i const*)(src2+i));
        __m128i br = _mm_loadu_si128((__m128i const*)(src2+i+16)))

        __m128i l_avg = avg16Bytes(tl, bl);
        __m128i r_avg = avg16Bytes(tr, br);

        _mm_storeu_si128((__m128i *)(dst+(i/2)), _mm_packus_epi16(l_avg, r_avg));
    }
}

注意:

  • 我意识到我的功能有轻微(一个一个)舍入错误,但我愿意接受这个。
  • 为清楚起见,我假设大小是32的倍数。

编辑:现在有一个github repository来实现这个问题的答案。最快的解决方案由用户Peter Cordes提供。有关详细信息,请参阅下面的文章:

__m128i avg16Bytes(const __m128i& row1, const __m128i& row2)
{
    // Average the first 16 values of src1 and src2:
    __m128i avg = _mm_avg_epu8(row1, row2);

    // Unpack and horizontal add:
    avg = _mm_maddubs_epi16(avg, _mm_set1_epi8(1));

    // Divide by 2:
    return  _mm_srli_epi16(avg, 1);
}

它通过计算(a+b)/2 + (c+d)/2而不是(a+b+c+d)/4作为我的原始实现,因此它具有相同的逐个舍入错误。

赞扬用户Paul R实施的解决方案速度是我的两倍,但确切地说:

__m128i avg16Bytes(const __m128i& row1, const __m128i& row2)
{
    // Unpack and horizontal add:
    __m128i row1 = _mm_maddubs_epi16(row1_u8, _mm_set1_epi8(1));
    __m128i row2 = _mm_maddubs_epi16(row2_u8, _mm_set1_epi8(1));

    // vertical add:
    __m128i avg = _mm_add_epi16(row1_avg, row2_avg);              

    // divide by 4:
    return _mm_srli_epi16(avg, 2);                     
}

2 个答案:

答案 0 :(得分:4)

如果您愿意接受两次使用pavgb的双舍入,那么您可以通过首先使用pavgb进行垂直平均来比Paul R的答案更快,切割需要解压缩到16位元素的数据量的一半。 (并允许一半的负载折叠到pavgb的内存操作数中,从而减少某些CPU的前端瓶颈。)

对于水平平均,您最好的选择可能仍为pmaddubsw set1(1)并移1,然后打包。

// SSSE3 version
// I used `__restrict__` to give the compiler more flexibility in unrolling
void average2Rows_doubleround(const uint8_t* __restrict__ src1, const uint8_t*__restrict__ src2,
                              uint8_t*__restrict__ dst, size_t size)
{
    const __m128i vk1 = _mm_set1_epi8(1);
    size_t dstsize = size/2;
    for (size_t i = 0; i < dstsize - 15; i += 16)
    {
        __m128i v0 = _mm_load_si128((const __m128i *)&src1[i*2]);
        __m128i v1 = _mm_load_si128((const __m128i *)&src1[i*2 + 16]);
        __m128i v2 = _mm_load_si128((const __m128i *)&src2[i*2]);
        __m128i v3 = _mm_load_si128((const __m128i *)&src2[i*2 + 16]);
        __m128i left  = _mm_avg_epu8(v0, v2);
        __m128i right = _mm_avg_epu8(v1, v3);

        __m128i w0 = _mm_maddubs_epi16(left, vk1);        // unpack and horizontal add
        __m128i w1 = _mm_maddubs_epi16(right, vk1);
        w0 = _mm_srli_epi16(w0, 1);                     // divide by 2
        w1 = _mm_srli_epi16(w1, 1);
        w0 = _mm_packus_epi16(w0, w1);                  // pack

        _mm_storeu_si128((__m128i *)&dst[i], w0);
    }
}

另一个选项是_mm_srli_epi16(v, 8),用于将奇数元素与每个水平对的偶数元素对齐。但由于没有截断的水平包,因此在打包之前必须_mm_and_si128(v, _mm_set1_epi16(0x00FF))两半。事实证明它比使用SSSE3 pmaddubsw慢,特别是没有AVX,需要额外的MOVDQA指令来复制寄存器。

void average2Rows_doubleround_SSE2(const uint8_t* __restrict__ src1, const uint8_t* __restrict__ src2, uint8_t* __restrict__ dst, size_t size)
{
    size /= 2;
    for (size_t i = 0; i < size - 15; i += 16)
    {
        __m128i v0 = _mm_load_si128((__m128i *)&src1[i*2]);
        __m128i v1 = _mm_load_si128((__m128i *)&src1[i*2 + 16]);
        __m128i v2 = _mm_load_si128((__m128i *)&src2[i*2]);
        __m128i v3 = _mm_load_si128((__m128i *)&src2[i*2 + 16]);
        __m128i left  = _mm_avg_epu8(v0, v2);
        __m128i right = _mm_avg_epu8(v1, v3);

        __m128i l_odd  = _mm_srli_epi16(left, 8);   // line up horizontal pairs
        __m128i r_odd  = _mm_srli_epi16(right, 8);

        __m128i l_avg = _mm_avg_epu8(left, l_odd);  // leaves garbage in the high halves
        __m128i r_avg = _mm_avg_epu8(right, r_odd);

        l_avg = _mm_and_si128(l_avg, _mm_set1_epi16(0x00FF));
        r_avg = _mm_and_si128(r_avg, _mm_set1_epi16(0x00FF));
        __m128i avg   = _mm_packus_epi16(l_avg, r_avg);          // pack
        _mm_storeu_si128((__m128i *)&dst[i], avg);
    }
}

对于AVX512BW,有_mm_cvtepi16_epi8,但IACA表示它在Skylake-AVX512上有2个uop,它只需1个输入并产生半宽输出。根据IACA,内存目的地形式是4个未融合的域uops(与reg,reg +单独的商店相同)。我不得不使用_mm_mask_cvtepi16_storeu_epi8(&dst\[i+0\], -1, l_avg);来获取它,因为gcc和clang无法将单独的_mm_store折叠到vpmovwb的内存目标中。 (没有非掩码存储内在的,因为编译器应该为你做这样做,就像它们将_mm_load折叠到典型ALU指令的存储器操作数中一样。)

它可能仅在缩小到1/4或1/8(cvtepi64_epi8)时才有用,而不是缩小到一半。或者可能有用,以避免需要第二次洗牌来处理_mm512_packus_epi16的车道内行为。使用AVX2,在_mm256_packus_epi16 [D C] [B A]之后,您有[D B | C A],您可以使用AVX2 _mm256_permute4x64_epi64 (__m256i a, const int imm8)修复以64位块进行随机播放。但是对于AVX512,您需要vpermq的矢量shuffle-control。 packus +一个fixup shuffle可能仍然是一个更好的选择。

执行此操作后,循环中不会留下很多向量指令,而可让编译器更严格地使用。遗憾的是,你的循环很难让编译器做得很好。   (这也有助于Paul R的解决方案,因为他从问题中复制了编译器不友好的循环结构。)

以gcc / clang可以更好地优化的方式使用循环计数器,并使用避免每次循环重新执行符号扩展的类型。

使用当前循环,gcc / clang实际上为i/2执行算术右移,而不是递增16(而不是32)并使用缩放索引寻址模式进行加载。似乎他们没有意识到i总是平等。

(full code + asm on Matt Godbolt's compiler explorer)

.LBB1_2:     ## clang's inner loop for int i, dst[i/2] version
    movdqu  xmm1, xmmword ptr [rdi + rcx]
    movdqu  xmm2, xmmword ptr [rdi + rcx + 16]
    movdqu  xmm3, xmmword ptr [rsi + rcx]
    movdqu  xmm4, xmmword ptr [rsi + rcx + 16]
    pavgb   xmm3, xmm1
    pavgb   xmm4, xmm2
    pmaddubsw       xmm3, xmm0
    pmaddubsw       xmm4, xmm0
    psrlw   xmm3, 1
    psrlw   xmm4, 1
    packuswb        xmm3, xmm4

    mov     eax, ecx         # This whole block is wasted instructions!!!
    shr     eax, 31
    add     eax, ecx
    sar     eax              # eax = ecx/2, with correct rounding even for negative `i`
    cdqe                     # sign-extend EAX into RAX

    movdqu  xmmword ptr [rdx + rax], xmm3
    add     rcx, 32          # i += 32
    cmp     rcx, r8
    jl      .LBB1_2          # }while(i < size-31)

gcc7.1并不是那么糟糕,(只是mov / sar / movsx),但是gcc5.x和6.x会分别指针增量src1和src2,以及商店的计数器/索引。 (完全是脑卒中的行为,特别是因为他们仍然使用-march=sandybridge。因为索引movdqu存储和非索引movdqu加载会给你带来最大的循环开销。)

无论如何,在循环中使用dstsize并乘以i而不是将其分开可以得到更好的结果。不同版本的gcc和clang可靠地将其编译成单个循环计数器,它们与负载的缩放索引寻址模式一起使用。你得到的代码如下:

    movdqa  xmm1, xmmword ptr [rdi + 2*rax]
    movdqa  xmm2, xmmword ptr [rdi + 2*rax + 16]
    pavgb   xmm1, xmmword ptr [rsi + 2*rax]
    pavgb   xmm2, xmmword ptr [rsi + 2*rax + 16]   # saving instructions with aligned loads, see below
    ...
    movdqu  xmmword ptr [rdx + rax], xmm1
    add     rax, 16
    cmp     rax, rcx
    jb      .LBB0_2

我使用size_t i来匹配size_t大小,以确保gcc不会浪费任何符号扩展指令或将其零扩展到指针的宽度。 (零扩展通常是免费的,因此unsigned sizeunsigned i可能没问题,并保存了几个REX前缀。)

你仍然可以摆脱cmp但是将索引计算为0,这将比我所做的更快地加速循环。我不确定让编译器变得愚蠢是多么容易,如果你向零计数,就省略cmp指令。但是,从对象的末尾开始索引是没有问题的。 src1+=size;。但是,如果你想使用一个未对齐的清理循环,它会使事情变得复杂。

在我的Skylake i7-6700k上(最大涡轮增压4.4GHz,但请查看时钟周期计数而不是时间)。使用g ++ 7.1,对于1024字节的100M代表与~3.3秒相比,这会产生~2.7秒的差异。

 Performance counter stats for './grayscale-dowscale-by-2.inline.gcc-skylake-noavx' (2 runs):

   2731.607950      task-clock (msec)         #    1.000 CPUs utilized            ( +-  0.40% )
             2      context-switches          #    0.001 K/sec                    ( +- 20.00% )
             0      cpu-migrations            #    0.000 K/sec                  
            88      page-faults:u             #    0.032 K/sec                    ( +-  0.57% )
11,917,723,707      cycles                    #    4.363 GHz                      ( +-  0.07% )
42,006,654,015      instructions              #    3.52  insn per cycle           ( +-  0.00% )
41,908,837,143      uops_issued_any           # 15342.186 M/sec                   ( +-  0.00% )
49,409,631,052      uops_executed_thread      # 18088.112 M/sec                   ( +-  0.00% )
 3,301,193,901      branches                  # 1208.517 M/sec                    ( +-  0.00% )
   100,013,629      branch-misses             #    3.03% of all branches          ( +-  0.01% )

   2.731715466 seconds time elapsed                                          ( +-  0.40% )

VS。相同的矢量化,但 int idst[i/2]会产生更高的循环开销(更多标量指令):

 Performance counter stats for './grayscale-dowscale-by-2.loopoverhead-aligned-inline.gcc-skylake-noavx' (2 runs):

   3314.335833      task-clock (msec)         #    1.000 CPUs utilized            ( +-  0.02% )
             4      context-switches          #    0.001 K/sec                    ( +- 14.29% )
             0      cpu-migrations            #    0.000 K/sec                  
            88      page-faults:u             #    0.026 K/sec                    ( +-  0.57% )
14,531,925,552      cycles                    #    4.385 GHz                      ( +-  0.06% )
51,607,478,414      instructions              #    3.55  insn per cycle           ( +-  0.00% )
51,109,303,460      uops_issued_any           # 15420.677 M/sec                   ( +-  0.00% )
55,810,234,508      uops_executed_thread      # 16839.040 M/sec                   ( +-  0.00% )
 3,301,344,602      branches                  #  996.080 M/sec                    ( +-  0.00% )
   100,025,451      branch-misses             #    3.03% of all branches          ( +-  0.00% )

   3.314418952 seconds time elapsed                                          ( +-  0.02% )

<强> VS。 Paul R的版本(针对较低的循环开销进行了优化):确切但较慢

Performance counter stats for './grayscale-dowscale-by-2.paulr-inline.gcc-skylake-noavx' (2 runs):

   3751.990587      task-clock (msec)         #    1.000 CPUs utilized            ( +-  0.03% )
             3      context-switches          #    0.001 K/sec                  
             0      cpu-migrations            #    0.000 K/sec                  
            88      page-faults:u             #    0.024 K/sec                    ( +-  0.56% )
16,323,525,446      cycles                    #    4.351 GHz                      ( +-  0.04% )
58,008,101,634      instructions              #    3.55  insn per cycle           ( +-  0.00% )
57,610,721,806      uops_issued_any           # 15354.709 M/sec                   ( +-  0.00% )
55,505,321,456      uops_executed_thread      # 14793.566 M/sec                   ( +-  0.00% )
 3,301,456,435      branches                  #  879.921 M/sec                    ( +-  0.00% )
   100,001,954      branch-misses             #    3.03% of all branches          ( +-  0.02% )

   3.752086635 seconds time elapsed                                          ( +-  0.03% )

VS。 Paul R的原始版本,带有额外的循环开销:

Performance counter stats for './grayscale-dowscale-by-2.loopoverhead-paulr-inline.gcc-skylake-noavx' (2 runs):

   4154.300887      task-clock (msec)         #    1.000 CPUs utilized            ( +-  0.01% )
             3      context-switches          #    0.001 K/sec                  
             0      cpu-migrations            #    0.000 K/sec                  
            90      page-faults:u             #    0.022 K/sec                    ( +-  1.68% )
18,174,791,383      cycles                    #    4.375 GHz                      ( +-  0.03% )
67,608,724,157      instructions              #    3.72  insn per cycle           ( +-  0.00% )
66,937,292,129      uops_issued_any           # 16112.769 M/sec                   ( +-  0.00% )
61,875,610,759      uops_executed_thread      # 14894.350 M/sec                   ( +-  0.00% )
 3,301,571,922      branches                  #  794.736 M/sec                    ( +-  0.00% )
   100,029,270      branch-misses             #    3.03% of all branches          ( +-  0.00% )

   4.154441330 seconds time elapsed                                          ( +-  0.01% )

请注意,分支未命中与重复计数大致相同:内部循环每次都在最后错误预测。展开以使循环迭代计数保持在约22以下将使得模式足够短以使Skylake的分支预测器在大多数时间正确地预测未采用的条件。分支错误预测是我们通过管道每循环不会达到~4.0微秒的唯一原因,因此避免分支未命中会将IPC从3.5提高到4.0以上(cmp / jcc宏融合将2条指令放在一个uop中)。

即使您在L2缓存带宽(而不是前端)上遇到瓶颈,这些分支未命中可能也会受到影响。但是我没有测试过:我的测试只是在Paul R的测试工具的函数调用周围包含一个for()循环,所以L1D缓存中的所有内容都很热门。内循环的32次迭代接近于最坏情况:对于频繁的错误预测而言足够低,但不能低到分支预测可以拾取模式并避免它们。

我的版本应该每次迭代运行3个周期,仅在前端,英特尔Sandybridge及更高版本上瓶颈。 (Nehalem每个时钟会产生一次负载瓶颈。)

有关融合域与未融合域uops和perf计数器的详情,请参阅http://agner.org/optimize/Can x86's MOV really be "free"? Why can't I reproduce this at all?

更新:clang为你展开,至少当大小是编译时常量时...奇怪的是,它甚至展开了dst[i/2]函数的非内联版本(使用未知size),但不是低环路开销版本。

使用clang++-4.0 -O3 -march=skylake -mno-avx,我的版本(编译器按2展开)运行:9.61G周期,100M iters(2.2s)。 (35.6G uops发布(融合域),45.0G uops执行(未融合域),接近零分支未命中。)可能不会在前端出现瓶颈,但AVX仍然会受到伤害。

Paul R&#s;(也由2展开)以12.29G周期运行100M iters(2.8s)。发布了48.4G uops(融合域),51.4G uops执行(未融合域)。对于4.08 IPC,50.1G指令可能仍然是前端的瓶颈(因为在销毁寄存器之前需要一些movdqa指令来复制寄存器)。 AVX有助于非破坏性矢量指令,即使没有AVX2也可以使用更宽的整数矢量。

通过仔细编码,您应该能够很好地处理运行时变量大小。

使用对齐的指针和对齐的加载,因此编译器可以将pavgb与内存操作数一起使用,而不是使用单独的无符号加载指令。这意味着更少的指令和更少的前端uop,这是这个循环的瓶颈。

这对Paul的版本没有帮助,因为只有pmaddubsw的第二个操作数可以来自内存,而那个被视为有符号字节的操作数。如果我们使用_mm_maddubs_epi16(_mm_set1_epi8(1), v0);,则16位乘法结果将是符号扩展而不是零扩展。所以1+255会出现0而不是256。

折叠负载需要与SSE对齐,但不能与AVX对齐。但是,on Intel Haswell/Skylake, indexed addressing modes can only stay micro-fused with instructions which read-modify-write their destination register. vpavgb xmm0, xmm0, [rsi+rax*2]在Haswell / Skylake发布到核心的无序部分之前未被层压到2 uops,但pavgb xmm1, [rsi+rax*2]可以保持微融合通过的方式,所以它作为一个单一的uop发布。前端问题瓶颈是主流x86 CPU上每个时钟4个融合域uop,除了Ryzen(即不是Atom / Silvermont)。将一半的负载折叠到内存操作数有助于除Sandybridge / Ivybridge和所有AMD CPU之外的所有Intel CPU。

gcc和clang会在内联到使用alignas(32)的测试函数时折叠负载,即使您使用_mm_loadu内在函数也是如此。他们知道数据是一致的,并且可以利用。

奇怪的事实:在启用了AVX代码生成(-march=native)的情况下编译128b矢量化代码实际上会降低Haswell / Skylake的速度,因为它会使所有4个加载问题成为单独的uops,即使它们是&#39 ;重新设置vpavgb的内存操作数,并且没有AVX会避免的任何movdqa寄存器复制指令。 (无论如何,即使对于仍然只使用128b向量的手动矢量化代码,AVX也会提前出现,因为3操作数指令不会破坏其中一个输入。)在这种情况下,13,53G cycles ( +- 0.05% )或{{1在{2.7}秒内从3094.195773 ms ( +- 0.20% )周期开始。 uops_issued = 11.92G,高于48.508G。指令计数和uops_executed计数基本相同。

OTOH,实际的256b AVX2版本的运行速度略低于两倍。一些展开以减少前端瓶颈肯定会有所帮助。根据@ Mysticial的测试,AVX512版本在Skylake-AVX512 Xeons上的运行速度可能接近4倍,但可能会导致ALU吞吐量出现瓶颈,因为当RS中有任何512b uop等待执行时,SKX会关闭执行端口1。 。 (这解释了为什么pavgb zmm has 1 per clock throughput while pavgb ymm is 2 per clock.。)

要使两个输入行对齐,将图像数据存储为行格式为16的倍数,即使实际图像尺寸为奇数。您的存储空间不必与实际图像尺寸相匹配。

如果你只能对齐源或目标(例如,因为你要缩小从源图像中奇数列开始的区域),你应该仍然可以对齐你的源指针。

英特尔优化手册建议您调整目标位置而不是源位置,如果您无法对齐两者,但执行4倍于商店的负载可能会改变平衡。

要在开始/结束时处理未对齐,请从开头和结尾执行可能重叠的未对齐像素矢量。商店与其他商店重叠是很好的,因为dst与src分开,你可以重做部分重叠的矢量。

在Paul的测试41,908中,我刚刚在每个数组前添加了main()

AVX2:

compile one version with -march=native以来,您可以使用alignas(32)在编译时轻松检测到AVX2。对于128b和256b手动矢量化,没有简单的方法可以使用完全相同的代码。所有内在函数都有不同的名称,因此即使没有其他差异,您通常也需要复制所有内容。

(有一些内部函数的C ++包装器库使用运算符重载和函数重载来让你编写一个在不同宽度的向量上使用相同逻辑的模板化版本。例如Agner Fog的VCL很好,但除非您的软件是开源的,否则您无法使用它,因为它已获得GPL许可并且您希望分发二进制文件。)

要在二进制分发版本中利用AVX2,您必须进行运行时检测/分派。在这种情况下,您希望调度到循环遍历行的函数的版本,因此您不会在循环内部对行进行调度开销。或者只是让该版本使用SSSE3。

答案 1 :(得分:3)

这是一个使用较少指令的实现。我没有对你的代码进行基准测试,所以它可能不会明显更快:

void average2Rows(const uint8_t* src1, const uint8_t* src2, uint8_t* dst, int size)
{
    const __m128i vk1 = _mm_set1_epi8(1);

    for (int i = 0; i < size - 31; i += 32)
    {
        __m128i v0 = _mm_loadu_si128((__m128i *)&src1[i]);
        __m128i v1 = _mm_loadu_si128((__m128i *)&src1[i + 16]);
        __m128i v2 = _mm_loadu_si128((__m128i *)&src2[i]);
        __m128i v3 = _mm_loadu_si128((__m128i *)&src2[i + 16]);

        __m128i w0 = _mm_maddubs_epi16(v0, vk1);        // unpack and horizontal add
        __m128i w1 = _mm_maddubs_epi16(v1, vk1);
        __m128i w2 = _mm_maddubs_epi16(v2, vk1);
        __m128i w3 = _mm_maddubs_epi16(v3, vk1);

        w0 = _mm_add_epi16(w0, w2);                     // vertical add
        w1 = _mm_add_epi16(w1, w3);

        w0 = _mm_srli_epi16(w0, 2);                     // divide by 4
        w1 = _mm_srli_epi16(w1, 2);

        w0 = _mm_packus_epi16(w0, w1);                  // pack

        _mm_storeu_si128((__m128i *)&dst[i / 2], w0);
    }
}

测试工具:

#include <stdio.h>
#include <stdlib.h>
#include <tmmintrin.h>

void average2Rows_ref(const uint8_t* row1, const uint8_t* row2, uint8_t* dst, int size)
{
    for (int i = 0; i < size - 1; i += 2)
    {
        dst[i / 2] = (row1[i] + row1[i + 1] + row2[i] + row2[i + 1]) / 4;
    }
}

void average2Rows(const uint8_t* src1, const uint8_t* src2, uint8_t* dst, int size)
{
    const __m128i vk1 = _mm_set1_epi8(1);

    for (int i = 0; i < size - 31; i += 32)
    {
        __m128i v0 = _mm_loadu_si128((__m128i *)&src1[i]);
        __m128i v1 = _mm_loadu_si128((__m128i *)&src1[i + 16]);
        __m128i v2 = _mm_loadu_si128((__m128i *)&src2[i]);
        __m128i v3 = _mm_loadu_si128((__m128i *)&src2[i + 16]);

        __m128i w0 = _mm_maddubs_epi16(v0, vk1);        // unpack and horizontal add
        __m128i w1 = _mm_maddubs_epi16(v1, vk1);
        __m128i w2 = _mm_maddubs_epi16(v2, vk1);
        __m128i w3 = _mm_maddubs_epi16(v3, vk1);

        w0 = _mm_add_epi16(w0, w2);                     // vertical add
        w1 = _mm_add_epi16(w1, w3);

        w0 = _mm_srli_epi16(w0, 2);                     // divide by 4
        w1 = _mm_srli_epi16(w1, 2);

        w0 = _mm_packus_epi16(w0, w1);                  // pack

        _mm_storeu_si128((__m128i *)&dst[i / 2], w0);
    }
}

int main()
{
    const int n = 1024;

    uint8_t src1[n];
    uint8_t src2[n];
    uint8_t dest_ref[n / 2];
    uint8_t dest_test[n / 2];

    for (int i = 0; i < n; ++i)
    {
        src1[i] = rand();
        src2[i] = rand();
    }

    for (int i = 0; i < n / 2; ++i)
    {
        dest_ref[i] = 0xaa;
        dest_test[i] = 0x55;
    }

    average2Rows_ref(src1, src2, dest_ref, n);
    average2Rows(src1, src2, dest_test, n);

    for (int i = 0; i < n / 2; ++i)
    {
        if (dest_test[i] != dest_ref[i])
        {
            printf("%u %u %u %u: ref = %u, test = %u\n", src1[2 * i], src1[2 * i + 1], src2[2 * i], src2[2 * i + 1], dest_ref[i], dest_test[i]);
        }
    }

    return 0;
}

请注意,SIMD版本的输出与标量参考代码的输出完全匹配(没有“off by one”舍入错误)。