使用 intel 内在函数将压缩的 8 位整数乘以浮点向量

Posted

技术标签:

【中文标题】使用 intel 内在函数将压缩的 8 位整数乘以浮点向量【英文标题】:Multiply packed 8 bit integers by vectors of floats using intel intrinsics 【发布时间】:2020-07-01 14:15:46 【问题描述】:

我正在编写一个大量使用英特尔内部函数(不包括 AVX512)的软件光栅化器。颜色由 32 位无符号表示,实际上只是 4 个压缩的 8 位颜色 (RGBA)。因此,一个 8 种颜色的向量可以保存在单个 __mm256 颜色变量中。但是,我需要通过将单个颜色乘以浮点数来操作此数组中的单个颜色。换句话说,我可能有另一个浮点/ps 值向量,__mm256 rLight,我想将颜色向量中 R 的相应 8 个无符号位乘以 rLight 变量中的浮点数。我找不到任何理智的方法来做到这一点。看来我需要做的是将感兴趣的 8 个字节提取到 __mm256 浮点数组中,然后进行乘法运算,然后转换回无符号并将它们放回原始数组中,但我很挣扎。

任何看起来很有希望的说明将不胜感激。

【问题讨论】:

您真的需要将它们乘以浮点数还是该因子的定点 8 位近似值就足够了? 那么浮点值(应该)总是小于 1。所以我真的需要做一个除法。出于这个原因,我认为转换为浮点数可能会使这更简单。 例如,乘以 0 到 255 之间的值并将结果向右移动 8 就足够了吗? (或 0 到 2^16-1 之间的值并移位 16) 【参考方案1】:

8 种颜色的向量可以保存在单个 __mm256 颜色变量中。

这不是最好的方法。添加 10+ 位颜色深度、伽马校正或颜色分级将非常困难。为获得最佳性能,请考虑改用 16 位整数或浮点数。

我找不到任何理智的方法来做到这一点。

    将您的浮点数转换为 15 位或 16 位定点。最快的方法是滥用 IEEE 表示,一个 FMA 指令来缩放 + 偏移浮点数,因此 [0..1] 范围对应于尾数的最低有效 15-16 位,然后位转换浮点数为整数,然后减去一个 int32 数字按位等于浮点偏移值。看看我如何处理 64 位双精度 https://github.com/Const-me/DtsDecoder/blob/7812fa32fbdc8b45e6b7dcd66aef1a58e104e089/libdcadec/interpolator_float.cpp#L135-L174 相同的方法可用于 32 位浮点数,寄存器中的所有 8 个浮点数只有 2 条指令,_mm256_fmadd_ps 和 _mm256_sub_epi32。

    使用 _mm256_packus_epi32 复制通道,同时将 32 位压缩为 16。请注意,指令使用饱和,将自动剪辑到 [0 .. 0xFFFF],即您不必在剪辑上浪费 CPU 周期。

    加载颜色。

    现在是时候扩大规模了,这是一种方法:

     inline __m256i scaleBytes( __m256i rgba, __m256i mul )
     
         __m256i low = _mm256_and_si256( rgba, _mm256_set1_epi16( 0xFF ) );
         __m256i high = _mm256_and_si256( rgba, _mm256_set1_epi16( 0xFF00 ) );
         low = _mm256_mulhi_epu16( low, mul );
         high = _mm256_mulhi_epu16( high, mul );
         high = _mm256_and_si256( high, _mm256_set1_epi16( 0xFF00 ) );
         return _mm256_or_si256( low, high );
     
    

    如果你想要更好的四舍五入,你可能需要调整上面的代码,上面的版本有一个错误,因为 0xFF * 0xFFFF = FEFF01 即你会在乘以 1.0 浮点数后得到 0xFE。修复的一个好方法是使用 1.15 固定点而不是 0.16,缩放浮点数以便 1.0 映射到 0x8000,并向 scaleBytes 函数添加几个位移指令。在第 2 步之后,您还需要将缩放值裁剪为 0x8000 上限,一条 _mm256_min_epu16 指令即可。

更新:我刚刚意识到,对于第 1 步,您不需要缩放,只需偏移即可。

// Test values
__m256 floats = _mm256_setr_ps( -1, 0, 0.11f, 0.33f, 0.99f, 1, 1.11f, 12 );

// Floats have 23 bits of mantissa.
// We want [0..1] to map to the least significant 15 of them.
// Therefore, we need to offset the floats by 2 ^ ( 23 - 15 ) = 2 ^ 8
constexpr float offsetFloat = 0x1p8f;
// Same value bit-casted to integer, too bad std::bit_cast only appeared in C++/20
// https://www.h-schmidt.net/FloatConverter/IEEE754.html
constexpr int offsetInt = 0x43800000;

// Compute the integers
floats = _mm256_add_ps( floats, _mm256_set1_ps( offsetFloat ) );
const __m256i result = _mm256_sub_epi32( _mm256_castps_si256( floats ), _mm256_set1_epi32( offsetInt ) );

// Print the result
alignas( 32 ) std::array<int, 8> scalars;
_mm256_store_si256( ( __m256i * )scalars.data(), result );
for( int i : scalars )
    printf( "0x%04x ", i );
printf( "\n" );

【讨论】:

谢谢。这太不可思议了。

以上是关于使用 intel 内在函数将压缩的 8 位整数乘以浮点向量的主要内容,如果未能解决你的问题,请参考以下文章

SSE 内在函数:将 32 位浮点数转换为 UNSIGNED 8 位整数

如何使用英特尔内在函数从 8 位整数数组构建 32 位整数?

使用 Intel 内在函数的位反向重新排序优化

调配 32 位 alpha 通道所需的 Intel 内在函数

SSE 将整数加载到 __m128

_mm_extract_epi8(...) 以非文字整数作为参数的内在函数