有/没有 SSE simd 操作的结果是不同的

Posted

技术标签:

【中文标题】有/没有 SSE simd 操作的结果是不同的【英文标题】:result with/without SSE simd operation is different 【发布时间】:2021-05-16 13:08:18 【问题描述】:

我正在尝试对数组的所有元素求和(无符号字符)

但 cv::Mat sum 的结果与 SSE 结果不同(如下代码)

使用 sse,数组结果的总和大于没有,但是为什么呢??

例如)我得到 2042115 的 sse 总和,但 cv::mat 的总和结果为 2041104。

__m128i srcVal;
        __m128i src16bitlo;
        __m128i src16bithi;
        __m128i src32bitlolo;
        __m128i src32bitlohi;
        __m128i src32bithilo;
        __m128i src32bithihi;
        __m128i vsum = _mm_setzero_si128();
    for (int i = 0; i < nSrcSize; i += 16)
    

        srcVal = _mm_loadu_si128((__m128i*) (pSrc + i));

        src16bitlo = _mm_unpacklo_epi8(srcVal, _mm_setzero_si128());
        src16bithi = _mm_unpackhi_epi8(srcVal, _mm_setzero_si128());

        src32bitlolo = _mm_unpacklo_epi16(src16bitlo, _mm_setzero_si128());
        src32bitlohi = _mm_unpackhi_epi16(src16bitlo, _mm_setzero_si128());
        src32bithilo = _mm_unpacklo_epi16(src16bithi, _mm_setzero_si128());
        src32bithihi = _mm_unpackhi_epi16(src16bithi, _mm_setzero_si128());

        vsum = _mm_add_epi32(src32bitlolo, vsum);
        vsum = _mm_add_epi32(src32bitlohi, vsum);
        vsum = _mm_add_epi32(src32bithilo, vsum);
        vsum = _mm_add_epi32(src32bithihi, vsum);


        // cout << "sumSrc : " << sumSrc << endl;
    
    int sumSrc = vsum.m128i_i32[0] + vsum.m128i_i32[1] + vsum.m128i_i32[2] + vsum.m128i_i32[3];
    //int check = sumSrc;

    int remainSize = nSrcSize % 16;
    if (remainSize > 0)
    
        unsigned char* arrTemp = new unsigned char[16]();  // 0으로 초기화
        memcpy(arrTemp, pSrc + nSrcSize - remainSize -1, remainSize);
        __m128i srcVal = _mm_loadu_si128((__m128i*)arrTemp);
        vsum = _mm_sad_epu8(srcVal, _mm_setzero_si128());
        sumSrc += vsum.m128i_i16[0] + vsum.m128i_i16[1] + vsum.m128i_i16[2] + vsum.m128i_i16[3] + vsum.m128i_i16[4] + vsum.m128i_i16[5] + vsum.m128i_i16[6] + vsum.m128i_i16[7];
    

【问题讨论】:

仅供参考,psadbw 反对零会将 unsigned char 的向量水平求和成两个 qwords 比您的 SIMD 循环更有效。 Fastest way to horizontally sum SSE unsigned byte vector。另请参阅 How to count character occurrences using SIMD 如何使用它。 哦,您已经在清理中使用了psadbw,但是您将所有 8 个 16 位元素相加,即使其中 6 个元素为零。 @PeterCordes 哦,我明白了。谢谢! 如果我的回答(也在这些 cmets 上展开)解决了您的问题,您可以在投票箭头下勾选“接受”它。 【参考方案1】:

你有 2 个错误:

i &lt; nSrcSize 在最终向量延伸超过nSrcSize 时可能为真。由于您已经在使用带符号的int i,因此您可以使用i &lt; nSrcSize - 15 来查找可以加载从i+0i+15 的完整16 个字节的最高i 值。或者,如果您使用的是 size_t,请使用 nSrcSize &amp; -16U

new unsigned char[16]() 不会将内存归零,因此您正在汇总一些额外的垃圾。你确实需要new,而你忘记删除它,所以你正在泄漏内存!您可以使用本地数组,而不是动态分配任何东西。

alignas(16) unsigned char arrTemp[16] = 0;  // implicitly initializes later elements to 0

但是可变大小的 memcpy 对效率并不好,重新加载 memcpy 结果会导致存储转发停止。 OTOH,你可以只 _mm_add_epi32(vsum, cleanup_sad) 并且只做一个水平向量和。

更有效的方法可能是根据大小对自己进行分支(而不是将工作传递给 memcpy)并使用 SIMD 加载执行 8 字节和 4 字节块。或者,一旦剩下的字节数少于 8 字节,就执行一个不会跨越缓存线边界的 8 字节加载以获取全部数据。

检查从您想要的第一个字节开始的加载是否会跨越 64 字节边界。如果不是,请执行 64 位左移以移入零,您可以安全地进行 hsum。如果是,则使用所需的 last 字节进行 ends 的加载,然后进行右移。您必须将班次计数计算为8 * (8 - bytes_to_keep)。您可以使用标量移位,然后将 _mm_cvtsi64_si128 转换为 SIMD 向量,或者直接使用 _mm_loadl_epi64 (movq) 并使用 SIMD 移位。 (不幸的是,SSE/AVX 没有可变计数字节移位,只有位移,并且计数需要在另一个 SIMD 向量中。)


仅供参考,psadbw 反对零会将 unsigned char 的向量水平总和成两个 qwords,这比您的 SIMD 循环更有效。 Fastest way to horizontally sum SSE unsigned byte vector。另请参阅 How to count character occurrences using SIMD 如何在外部循环中使用它来将字节向量累积到具有更宽元素的 SIMD 向量中。

您已经在清理中使用了psadbw,但是您将所有八个 16 位元素相加,即使其中六个是零。

【讨论】:

我更改了我的代码。但似乎问题出在代码的第一部分,而不是处理剩余部分。 @LakesideStar:哦,您还有另一个错误:当最终向量超出nSrcSize 时,i &lt; nSrcSize 可能为真。由于您已经在使用带符号的int i,您可以使用i &lt; nSrcSize - 15 来查找可以加载从i+0i+15 的完整16 个字节的最高i 值。或者,如果您使用的是 size_t,请使用 nSrcSize &amp; -16U

以上是关于有/没有 SSE simd 操作的结果是不同的的主要内容,如果未能解决你的问题,请参考以下文章

SIMD/SSE:短点积和短最大值

SIMD指令集——一条指令操作多个数,SSE,AVX都是,例如:乘累加,Shuffle等

SSE3指令有啥功能?

如何将无符号整数加载到 SIMD 中

Simd matmul 程序给出不同的数值结果

SIMD和动态内存分配[重复]