在字节数组上,4 个字节的点积组与 4 个小常量(有效地使用 SIMD)?

问题描述 投票:0回答:1

我有一个特殊的要求需要有效地满足。 (SIMD,也许?)

src
是一个字节数组。数组中每组4个字节需要处理为:

  • src[0]
    的低半字节乘以常数
    A
  • src[1]
    的低半字节乘以常数
    B
  • src[2]
    的低半字节乘以常数
    C
  • src[3]
    的低半字节乘以常数
    D

将以上四部分相加得到

result

移至下 4 组字节并重新计算

result
(冲洗并重复直到字节数组末尾)。

由于涉及的所有数字都非常小,所以

result
保证很小(甚至适合一个字节)。然而,
result
的数据类型可以灵活地支持有效的算法。

有什么建议/提示/技巧可以比以下伪代码更快吗?:

for (int i=0; i< length; i+=4)
{
  result = (src[i] & 0x0f) * A + (src[i+1] & 0x0f) * B + (src[i+2] & 0x0f) * C + (src[i+3] & 0x0f) * D;
}

顺便说一句,

result
然后形成一个高阶数组的索引。

这个特定的循环非常重要,实现语言不构成障碍。可以选择 C#、C 或 MASM64 语言

c# c assembly masm simd
1个回答
0
投票

这是一个如何使用 SSE 内在函数有效地做到这一点的示例。

#include <stdint.h>
#include <emmintrin.h>  // SSE 2
#include <tmmintrin.h>  // SSSE 3
#include <smmintrin.h>  // SSE 4.1

// Vector constants for dot4Sse function
struct ConstantVectorsSse
{
    __m128i abcd;
    __m128i lowNibbleMask;
    __m128i zero;
};

// Pack 4 bytes into a single uint32_t value
uint32_t packBytes( uint32_t a, uint32_t b, uint32_t c, uint32_t d )
{
    b <<= 8;
    c <<= 16;
    d <<= 24;
    return a | b | c | d;
}

// Initialize vector constants for dot4Sse function
struct ConstantVectorsSse makeConstantsSse( uint8_t a, uint8_t b, uint8_t c, uint8_t d )
{
    struct ConstantVectorsSse cv;
    cv.abcd = _mm_set1_epi32( (int)packBytes( a, b, c, d ) );
    cv.lowNibbleMask = _mm_set1_epi8( 0x0F );
    cv.zero = _mm_setzero_si128();
    return cv;
}

// Dot products of 4 groups of 4 bytes in memory against 4 small constants
// Returns a vector of 4 int32 lanes
__m128i dot4Sse( const uint8_t* rsi, const struct ConstantVectorsSse* cv )
{
    // Load 16 bytes, and mask away higher 4 bits in each byte
    __m128i v = _mm_loadu_si128( ( const __m128i* )rsi );
    v = _mm_and_si128( cv->lowNibbleMask, v );

    // Compute products, add pairwise
    v = _mm_maddubs_epi16( cv->abcd, v );

    // Final reduction step, add adjacent pairs of uint16_t lanes
    __m128i high = _mm_srli_epi32( v, 16 );
    __m128i low = _mm_blend_epi16( v, cv->zero, 0b10101010 );
    return _mm_add_epi32( high, low );
}

代码使用

pmaddubsw
SSSE3 指令进行乘法和第一步归约,然后在向量中添加偶数/奇数
uint16_t
通道。

上面的代码假设您的 ABCD 数字是无符号字节。如果它们已签名,您将需要翻转

_mm_maddubs_epi16
内在参数的顺序,并在第二个归约步骤中使用不同的代码,
_mm_slli_epi32( v, 16 )
_mm_add_epi16
_mm_srai_epi32( v, 16 )

如果您有 AVX2,则升级很简单,请将

__m128i
替换为
__m256i
,将
_mm_something
替换为
_mm256_something

如果您输入的长度不一定是 4 组的倍数,请注意您需要对最后一批不完整的数字进行特殊处理。如果没有

_mm_maskload_epi32
AVX2 指令,这是加载 4 字节批次的不完整向量的一种可能方法:

__m128i loadPartial( const uint8_t* rsi, size_t rem )
{
    assert( rem != 0 && rem < 4 );
    __m128i v;
    switch( rem )
    {
    case 1:
        v = _mm_cvtsi32_si128( *(const int*)( rsi ) );
        break;
    case 2:
        v = _mm_cvtsi64_si128( *(const int64_t*)( rsi ) );
        break;
    case 3:
        v = _mm_cvtsi64_si128( *(const int64_t*)( rsi ) );
        v = _mm_insert_epi32( v, *(const int*)( rsi + 8 ), 2 );
        break;
    }
    return v;
}
© www.soinside.com 2019 - 2024. All rights reserved.