【问题标题】:Sum reduction of unsigned bytes without overflow, using SSE2 on Intel在 Intel 上使用 SSE2 减少无符号字节的总和而不溢出
【发布时间】:2012-06-11 13:13:33
【问题描述】:

我试图在 Intel i3 处理器上找到 32 个元素(每个 1 字节数据)的总和减少。我这样做了:

s=0; 
for (i=0; i<32; i++)
{
    s = s + a[i];
}  

但是,它需要更多时间,因为我的应用程序是一个需要更少时间的实时应用程序。 请注意,最终总和可能超过 255。

有没有一种方法可以使用低级 SIMD SSE2 指令来实现?不幸的是,我从未使用过 SSE。为此,我尝试搜索 sse2 函数,但它也不可用。 (sse)是否保证减少这种小型问题的计算时间?

有什么建议吗??

注意:我已经使用 OpenCL 和 CUDA 实现了类似的算法,并且效果很好,但仅在问题规模很大时才有效。对于小型问题,开销成本更高。不知道它在 SSE 上是如何工作的

【问题讨论】:

  • 是的,最终的总和可能大于 255

标签: x86 sse simd sse2 sse3


【解决方案1】:

您可以滥用PSADBW 快速计算小的水平总和。

类似这样的:(未测试)

pxor xmm0, xmm0
psadbw xmm0, [a + 0]
pxor xmm1, xmm1
psadbw xmm1, [a + 16]
paddw xmm0, xmm1
pshufd xmm1, xmm0, 2
paddw xmm0, xmm1 ; low word in xmm0 is the total sum

尝试的内在函数版本:

我从不使用内在函数,所以这段代码可能毫无意义。不过,反汇编看起来还不错。

uint16_t sum_32(const uint8_t a[32])
{
    __m128i zero = _mm_xor_si128(zero, zero);
    __m128i sum0 = _mm_sad_epu8(
                        zero,
                        _mm_load_si128(reinterpret_cast<const __m128i*>(a)));
    __m128i sum1 = _mm_sad_epu8(
                        zero,
                        _mm_load_si128(reinterpret_cast<const __m128i*>(&a[16])));
    __m128i sum2 = _mm_add_epi16(sum0, sum1);
    __m128i totalsum = _mm_add_epi16(sum2, _mm_shuffle_epi32(sum2, 2));
    return totalsum.m128i_u16[0];
}

【讨论】:

  • 能否为上述内容编写英特尔® C++ 编译器内部等效项?
  • @gpuguy 我试过了,但我从不使用内在函数,所以我可能搞砸了。 reinterpret_cast 看起来也不太好看,但我不知道如何摆脱它。
  • 要对int8_t(而不是uint8_t)使用相同的技巧:范围转移到无符号(与0x80 异或),然后从总数中减去16 * 0x80。见this patch for Agner Fog's vector class library for an example with intrinsics。同样的想法适用于an AVX2 ymm vector)。
【解决方案2】:

这有点冗长,但它仍然应该比标量代码快至少 2 倍:

uint16_t sum_32(const uint8_t a[32])
{
    const __m128i vk0 = _mm_set1_epi8(0);   // constant vector of all 0s for use with _mm_unpacklo_epi8/_mm_unpackhi_epi8
    __m128i v = _mm_load_si128(a);          // load first vector of 8 bit values
    __m128i vl = _mm_unpacklo_epi8(v, vk0); // unpack to two vectors of 16 bit values
    __m128i vh = _mm_unpackhi_epi8(v, vk0);
    __m128i vsum = _mm_add_epi16(vl, vh);
    v = _mm_load_si128(&a[16]);             // load second vector of 8 bit values
    vl = _mm_unpacklo_epi8(v, vk0);         // unpack to two vectors of 16 bit values
    vh = _mm_unpackhi_epi8(v, vk0);
    vsum = _mm_add_epi16(vsum, vl);
    vsum = _mm_add_epi16(vsum, vh);
    // horizontal sum
    vsum = _mm_add_epi16(vsum, _mm_srli_si128(vsum, 8));
    vsum = _mm_add_epi16(vsum, _mm_srli_si128(vsum, 4));
    vsum = _mm_add_epi16(vsum, _mm_srli_si128(vsum, 2));
    return _mm_extract_epi16(vsum, 0);
}

注意a[]需要16字节对齐。

您可以使用_mm_hadd_epi16 改进上述代码。

【讨论】:

  • 如何确保 a[] 是 16 字节对齐的?在 SSE 中是否有类似于 CUDA 中的 __align__(16) 的东西?
  • 这取决于您使用的编译器和操作系统 - 例如对于非动态分配的 gcc,使用 __attribute__ ((aligned(16))) - 对于 Linux 上的动态分配,使用 memalign()posix_memalign()
  • 将不得不对此投反对票;它有效,但psadbw 是正确答案。对于有符号的int8_t,您可以使用xor 将范围转移到无符号以将0x80 添加到每个字节,并从结果中减去16 * 0x80。 (参见this patch for Agner Fog's vector class library,例如内部函数。同样的想法适用于an AVX2 ymm vector)。但是这里的 OP 似乎已经没有签名了,所以 psadbw 是一个巨大的胜利。
  • @PeterCordes:好吧,我确实说过“有点啰嗦”。 ;-) 但是,是的,哈罗德的答案是更好的解决方案(我当然赞成)。我可能应该删除这个,因为它实际上没有任何用处。
【解决方案3】:

还有另一种方法可以使用 SSE 指令查找数组中所有元素的总和。该代码使用以下 SSE 构造。

  • __m256 寄存器
  • _mm256_store_ps(float *a, __m256 b)
  • _mm256_add_ps(__m256 a, __m256 b)

该代码适用于任何大小的浮点数组。

float sse_array_sum(float *a, int size)
{
    /*
     *   sum += a[i] (for all i in domain)
     */

    float *sse_sum, sum=0;
    if(size >= 8)
    {
        // sse_sum[8]
        posix_memalign((void **)&sse_sum, 32, 8*sizeof(float));

        __m256 temp_sum;
        __m256* ptr_a = (__m256*)a;
        int itrs = size/8-1;

        // sse_sum[0:7] = a[0:7]
        temp_sum = *ptr_a;
        a += 8;
        ptr_a++;

        for(int i=0; i<itrs; i++, ptr_a++, a+=8)
            temp_sum = _mm256_add_ps(temp_sum, *ptr_a);

        _mm256_store_ps(sse_sum, temp_sum);
        for(int i=0; i<8; i++)  sum += sse_sum[i];
    }

    // if size is not divisible by 8
    int rmd_itrs = size%8;
    // Note: a is pointing to remainder elements
    for(int i=0; i<rmd_itrs; i++)   sum += a[i];

    return sum;
}


float seq_array_sum(float *a, int size)
{
    /*
     *  sum += a[i] (for all i)
     */

    float sum = 0;
    for(int i=0; i<size; i++)   sum += a[i];
    return sum;
}

基准测试:

大小 = 64000000
a[i] = 3141592.65358 对于域中的所有 i

顺序版本时间:194ms
SSE 版本时间:49ms

机器规格:

每个内核的线程数:2
每个插槽的核心数:2
插座:1
CPU 频率:1700.072
操作系统:Ubuntu

【讨论】:

  • 首先,这个问题是关于求和uint8_t[]。更重要的是,动态分配临时存储绝对不是赢家。只需像普通人一样使用__m256 sum 临时。 (或者更好的是,使用多个累加器来隐藏 FP 延迟。如果您想使用它们的本地“数组”,编译器通常会优化它并将它们全部保存在寄存器中)。并且绝对不要在内部循环中使用_mm256_store_ps(),尽管这也可能会优化掉。
  • 另外,如果a 未按 32 对齐,则您的代码不安全;您取消引用 __m256* 而不是使用 _mm256_loadu_ps
  • 一个 AVX __m256 版本的 SSE reduction of float vector 是你想要的。注意它的内部循环是多么简单。最后的水平总和见Fastest way to do horizontal SSE vector sum (or other reduction)。存储到alignas(32) float tmp[8] 是一种选择,但您可以通过随机播放做得更好。此外,使用多个累加器展开主循环以隐藏 FP 延迟,如 this Q&A
  • 仅供参考:__m256 东西需要 AVX。将所有 x86 SIMD 的东西统称为“SSE”并不是完全错误的,但通常人们至少将 SSE* 与 AVX1 / AVX2 / FMA 与 AVX512 分开。无论如何,感谢您尝试提供帮助,但不幸的是,这个答案并没有展示出做事的好方法,即使您将其发布在 SSE reduction of float vector 上,我也不得不对其投反对票,至少它会回答正确的问题.
  • 当你第一次学习一些东西时这很正常,不要因为你的第一次尝试过于复杂和不安全而感到难过(仅适用于对齐的a,并且会泄漏动态分配的内存 - 就像我刚才说的那样使用本地数组或使用随机播放来 hsum)。但这不会改变这样一个事实,即它对于其他初学者或任何人来说都不是一个很好的例子,因此也不是一个好的 Stack Overflow 答案。也许codereview.stackexchange.com 是发布此类尝试以获取有关如何正确执行此操作的反馈的好地方。
猜你喜欢
  • 1970-01-01
  • 2019-02-27
  • 1970-01-01
  • 2023-03-24
  • 2019-04-15
  • 2020-06-07
  • 1970-01-01
  • 1970-01-01
  • 2021-12-31
相关资源
最近更新 更多