8位SSE LERP

3

我一直在试图找到使用AMD64 SIMD指令实现lerp的最佳方法,以便用于大量u8值的处理,但我似乎无法找到正确的指令,而不需要使用所有SIMD扩展。

我目前正在使用的公式是

u8* a;
u8* b;
u8* result;
size_t count;
u16 total;
u16 progress;

u32 invertedProgress = total - progress;
for(size_t i = 0; i < count; i++){
    result[i] = (u8)((b[i] * progress + a[i] * invertedProgress) / total);
}

我认为它看起来会像这样:

u8* a;
u8* b;
u8* result;
size_t count;
u16 total;
u16 progress;

__m128i mmxZero;
__m128i mmxProgress;
__m128i mmxInvertedProgress;
__m128i mmxProductA;
__m128i mmxProductB;

mmxZero = _mm_xor_ps(zero, zero); // Is there a clear?

mmxProgress = Fill with progress;

mmxTotal = Fill with total;

mmxInvertedProgress = mmxTotal;
mmxInvertedProgress = _mm_unpacklo_epi8(mmxInvertedProgres, mmxZero);
mmxInvertedProgress = _mm_sub_epi8(mmxTotal, progress);

for(size_t i = 0; i < count; i += 8){
    mmxProductA = load A;
    // u8 -> u16
    mmxProductA = _mm_unpacklo_epi8(mmxProductA, mmxZero);
    
    mmxProductB = load B;
    // u8 -> u16
    mmxProductB = _mm_unpacklo_epi8(mmxProductB, mmxZero);

    // a * (total - progress)
    mmxProductA = _mm_mullo_epi16(mmxProductA, mmxInvertedProgress);
    // b * progress
    mmxProductB = _mm_mullo_epi16(mmxProductB, mmxProgress);

    // a * (total - progress) + b * progress
    mmxProductA = _mm_add_epi16(mmxProductA, mmxProductB);
    // (a * (total - progress) + b * progress) / total
    mmxProductA = _mm_div_epi16(mmxProductA, mmxTotal);

    mmxProductA = saturated u16 -> u8; 
    store result = maxProductA;
}

有一些东西我在指南里找不到,主要涉及值的加载和存储。

我知道有一些新的指令可以同时处理更多量的数据,但这个初步实现应该可以在旧芯片上运行。

对于这个示例,我也忽略了对齐和缓冲区溢出的潜在问题,因为我认为这超出了问题的范围。


1
_mm_div_epi16是一个库函数,而不是硬件指令的内部函数。即使AVX512也没有SIMD整数除法,只有浮点除法。英特尔的内部函数指南混淆地列出了他们的SVML库函数和真实指令的内部函数,可能是为了诱使人们购买他们的库。您可以使用左侧的复选框过滤掉它。 - Peter Cordes
1
此外,MMX 早于 SSE / SSE2,并且仅具有 64 位整数向量。我建议使用变量名 __m128i vzero = _mm_setzero_si128();_mm_set1_epi32(0)。不要在 C 源代码中尝试进行 xor-zero,让编译器为您进行优化。 - Peter Cordes
我老实说并没有发现这个集合是内置的,只是因为那是我找到的唯一清零的方法。在那个页面上有太多的内置函数! - gudenau
1个回答

1

很好的问题。正如您发现的那样,SSE没有整数除法指令,并且(与ARM NEON不同)它没有用于字节的乘法或FMA。

以下是我通常使用的方法。下面的代码将向量分成偶数/奇数字节,使用16位乘法指令单独进行缩放,然后将它们合并回字节。

// Linear interpolation is based on the following formula: x*(1-s) + y*s which can equivalently be written as x + s(y-x).
class LerpBytes
{
    // Multipliers are fixed point numbers in 16-bit lanes of these vectors, in 1.8 format
    __m128i mulX, mulY;

public:

    LerpBytes( uint16_t progress, uint16_t total )
    {
        // The source and result are bytes.
        // Multipliers only need 1.8 fixed point format, anything above that is wasteful.
        assert( total > 0 );
        assert( progress >= 0 );
        assert( progress <= total );

        const uint32_t fp = (uint32_t)progress * 0x100 / total;
        mulY = _mm_set1_epi16( (short)fp );
        mulX = _mm_set1_epi16( (short)( 0x100 - fp ) );
    }

    __m128i lerp( __m128i x, __m128i y ) const
    {
        const __m128i lowMask = _mm_set1_epi16( 0xFF );

        // Split both vectors into even/odd bytes in 16-bit lanes
        __m128i lowX = _mm_and_si128( x, lowMask );
        __m128i highX = _mm_srli_epi16( x, 8 );
        __m128i lowY = _mm_and_si128( y, lowMask );
        __m128i highY = _mm_srli_epi16( y, 8 );

        // That multiply instruction has relatively high latency, 3-5 cycles.
        // We're lucky to have 4 vectors to handle.
        lowX = _mm_mullo_epi16( lowX, mulX );
        lowY = _mm_mullo_epi16( lowY, mulY );
        highX = _mm_mullo_epi16( highX, mulX );
        highY = _mm_mullo_epi16( highY, mulY );

        // Add the products
        __m128i low = _mm_adds_epu16( lowX, lowY );
        __m128i high = _mm_adds_epu16( highX, highY );

        // Pack them back into bytes.
        // The multiplier was 1.8 fixed point, trimming the lowest byte off both vectors.
        low = _mm_srli_epi16( low, 8 );
        high = _mm_andnot_si128( lowMask, high );
        return _mm_or_si128( low, high );
    }
};

static void print( const char* what, __m128i v )
{
    printf( "%s:\t", what );
    alignas( 16 ) std::array<uint8_t, 16> arr;
    _mm_store_si128( ( __m128i * )arr.data(), v );
    for( uint8_t b : arr )
        printf( " %02X", (int)b );
    printf( "\n" );
}

int main()
{
    const __m128i x = _mm_setr_epi32( 0x33221100, 0x77665544, 0xBBAA9988, 0xFFEEDDCC );
    const __m128i y = _mm_setr_epi32( 0xCCDDEEFF, 0x8899AABB, 0x44556677, 0x00112233 );
    LerpBytes test( 0, 1 );
    print( "zero", test.lerp( x, y ) );
    test = LerpBytes( 1, 1 );
    print( "one", test.lerp( x, y ) );
    test = LerpBytes( 1, 2 );
    print( "half", test.lerp( x, y ) );
    test = LerpBytes( 1, 3 );
    print( "1/3", test.lerp( x, y ) );
    test = LerpBytes( 1, 4 );
    print( "1/4", test.lerp( x, y ) );
    return 0;
}

这看起来非常有前途,我会在有机会的时候尝试一下并回报。 - gudenau
有没有一种方法可以从指针中加载值到寄存器中? - gudenau
我可能也缺少一些愚蠢的东西,因为我没有看到反转_mm_setr_epi32的方法。 - gudenau
@gudenau 请查看 _mm_load_si128_mm_loadu_si128,还有其他类似的函数,但这两个是最常用于16字节整数向量的。 - Soonts

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接