我有一个特殊的要求需要有效地满足。 (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 语言
这是一个如何使用 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;
}