使用英特尔内在函数将 8 位整数乘以浮点向量
Multiply packed 8 bit integers by vectors of floats using intel intrinsics
我正在编写大量使用英特尔内在函数(不包括 AVX512)的软件光栅器。颜色由 32 位无符号表示,实际上只是 4 个打包的 8 位颜色 (RGBA)。因此,一个包含 8 种颜色的向量可以保存在一个 __mm256 颜色变量中。但是,我需要通过将单个颜色乘以浮点数来操纵该数组中的单个颜色。换句话说,我可能有另一个 float/ps 值的向量,__mm256 rLight,我想将颜色向量中 R 的相应 8 个无符号位乘以 rLight 变量中的浮点数。我找不到任何理智的方法来做到这一点。似乎我需要做的是将感兴趣的 8 个字节提取到 __mm256 浮点数组中,然后进行乘法运算,然后转换回无符号并将它们粘贴回原始数组中,但我正在努力。
任何看起来很有希望的说明将不胜感激。
a vector of 8 colors may be held in a single __mm256 color variable.
这不是最好的方法。很难添加 10+ 位色深、伽玛校正或颜色分级。为获得最佳性能,请考虑改用 16 位整数或浮点数。
I cannot find any sane way to do this.
将浮点数转换为 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 即你会得到 0xFE 之后乘以 1.0 浮点数。一个很好的修复方法是对缩放器使用 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" );
我正在编写大量使用英特尔内在函数(不包括 AVX512)的软件光栅器。颜色由 32 位无符号表示,实际上只是 4 个打包的 8 位颜色 (RGBA)。因此,一个包含 8 种颜色的向量可以保存在一个 __mm256 颜色变量中。但是,我需要通过将单个颜色乘以浮点数来操纵该数组中的单个颜色。换句话说,我可能有另一个 float/ps 值的向量,__mm256 rLight,我想将颜色向量中 R 的相应 8 个无符号位乘以 rLight 变量中的浮点数。我找不到任何理智的方法来做到这一点。似乎我需要做的是将感兴趣的 8 个字节提取到 __mm256 浮点数组中,然后进行乘法运算,然后转换回无符号并将它们粘贴回原始数组中,但我正在努力。
任何看起来很有希望的说明将不胜感激。
a vector of 8 colors may be held in a single __mm256 color variable.
这不是最好的方法。很难添加 10+ 位色深、伽玛校正或颜色分级。为获得最佳性能,请考虑改用 16 位整数或浮点数。
I cannot find any sane way to do this.
将浮点数转换为 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 即你会得到 0xFE 之后乘以 1.0 浮点数。一个很好的修复方法是对缩放器使用 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" );