将位解压缩为单精度浮点数的最快方法

Fastest way to unpack bits into single precision floats

这是平台特定的问题。速度至关重要。 将一个字节解包到一个由 8 个单精度浮点数组成的数组以便零映射到零,而一映射到一的最快方法是什么?

我最终使用 8 位掩码和 7 位移位解压缩为 8 个 int32,然后使用 AVX 指令将 int32 转换为浮点数。

我的平台 Windows 64 位 运行 支持 AVX(但不支持 AVX2)CPU。编译器:Visual Studio 2013.

谢谢。

void byteToFloat(const uint8_t               byteIn, 
                       float *const restrict floatOut)
{
     floatOut[0]=(byteIn&0x01)?1.0f:0.0f;
     floatOut[1]=(byteIn&0x02)?1.0f:0.0f;
     floatOut[2]=(byteIn&0x04)?1.0f:0.0f;
     floatOut[3]=(byteIn&0x08)?1.0f:0.0f;
     floatOut[4]=(byteIn&0x10)?1.0f:0.0f;
     floatOut[5]=(byteIn&0x20)?1.0f:0.0f;
     floatOut[6]=(byteIn&0x40)?1.0f:0.0f;
     floatOut[7]=(byteIn&0x80)?1.0f:0.0f;
}

In x86-64 architectures from both Intel and AMD, branch predication may be performed through the use of conditional move operations (cmove): a source operand is conditionally moved to the destination operand depending on the value of a flag register.

http://en.wikipedia.org/wiki/Branch_predication

循环、条件和遍历内存中的实际数组当然不是向量方式。所以这是另一个想法,尽管它在仅 AVX 中有点烦人。由于没有 AVX2,你几乎不能用 ymm 寄存器做任何事情(反正没什么用),只需使用两个 xmm 寄存器,然后最后 vinsertf128 高部分形成整个东西。只要 xmm 寄存器上的操作使用 VEX 编码指令,这样的混合就可以了(所以 'v' 放在所有内容的前面,即使它看起来没有必要)。

无论如何,我们的想法是在每个双字中放置一个字节的副本,并与每个通道的正确位进行比较以形成掩码。最后我们可以做一个按位与将掩码变成 0f 或 1f。

所以,首先在所有地方获取那个字节,假设它在 eax 中,并不重要:

vmovd xmm0, eax
vpshufd xmm0, xmm0, 0

提取正确的位:

vpand xmm0, xmm0, [low_mask]
vpand xmm1, xmm0, [high_mask]

掩码是1, 2, 4, 816, 32, 64, 128(这是内存顺序,如果你使用_mm_set_epi32它们必须相反)

比较形成掩码:

vpxor xmm2, xmm2, xmm2
vpcmpgtd xmm0, xmm0, xmm2
vpcmpgtd xmm1, xmm1, xmm2

合并:

vinsertf128 ymm0, ymm0, xmm1, 1

转成0f或1f:

vandps ymm0, ymm0, [ones]

ones 只是 1f 重复了 8 次。

我不知道这是否更快,但值得一试。此外,none 已经过测试。

我试图将它转换为内在函数,但我不知道我在做什么(而且它没有经过测试)。另外,请注意它使用 VEX 前缀进行编译,否则会导致昂贵的模式切换。

// broadcast
__m128i low = _mm_set1_epi32(mask);
__m128i high = _mm_set1_epi32(mask);
// extract bits
low = _mm_and_si128(low, _mm_set_epi32(8, 4, 2, 1));
high = _mm_and_si128(high, _mm_set_epi32(128, 64, 32, 16));
// form masks
low = _mm_cmpgt_epi32(low, _mm_setzero_si128());
high = _mm_cmpgt_epi32(high, _mm_setzero_si128());
// stupid no-op casts
__m256 low2 = _mm256_castps128_ps256(_mm_castsi128_ps(low));
__m128 high2 = _mm_castsi128_ps(high);
// merge
__m256 total = _mm256_insertf128_ps(low2, high2, 1);
// convert to 0f or 1f
total = _mm256_and_ps(total, _mm256_set1_ps(1.0f));

至少对于 GCC,生成 OK 代码。它使用 vbroadcastss 作为 set1(而不是我使用的 vpshufd),我不确定这个想法有多好(这意味着它必须通过内存)。

使用 AVX2 可以简单得多:

__m256i x = _mm256_set1_epi32(mask); 
x = _mm256_and_si256(x, _mm256_set_epi32(128, 64, 32, 16, 8, 4, 2, 1));
x = _mm256_cmpgt_epi32(x, _mm256_setzero_si256());
x = _mm256_and_si256(x, _mm256_set1_epi32(0x3F800000));
return _mm256_castsi256_ps(x);

预处理会不会更快? 2^8的可能性已经差不多了,不过话又说回来,把它分成两部分,也只有2^4 = 16个变量。

创建包含 16 个 "values" 的数组,其中每个值都是用 4 个具有正确值的浮点数填充的数组。那么您的成本仅为 2 *(将数据从预处理数组复制到新数组)。

我不太了解汇编,但两个副本应该比某些循环等更快。

unsigned char myByte; // input byte (pattern to create floats)
float preprocessingArrays[16][4] = {
    { 0.0f, 0.0f, 0.0f, 0.0f }, // 0000
    // ...
    { 1.0f, 1.0f, 1.0f, 1.0f }  // 1111
};

float result[8];
std::memcpy(&result[0], &preprocessingArrays[myByte >> 4][0], 16);
std::memcpy(&result[4], &preprocessingArrays[myByte & 15][0], 16);
// 16 = platform-specific -> floats should be 32bits -> 4bytes * 4 floats = 16

这是手写的,但如您所见,我的循环将包含两个 memcpy、一个位移位和一个二进制 AND 运算(或者只有一个,但更大,memcpy,如果您想对 2^ 进行预处理) 8 个值)。

对于仅 C(++) 的代码,我认为这会击败循环等,但汇编代码可能更快,我不太确定。 也许你可以使用汇编程序执行 memcpy 操作,一次读取整个 4 个浮点数,然后在另一个调用中写入它。 AVX 似乎最多支持 16 个 256 位寄存器,所以可能只计算从哪个寄存器(16 个可能值)复制值在哪里,这将非常快。

也不要自己写那么多代码,直接做个简单的程序打印预处理值给你,复制粘贴到原程序中:)

正如@RippeR 所暗示的那样,索引也是我的第一个猜测。

我的第二个猜测是这样的:

switch(theChar){
 break; case   0: result[0] = 0; ... result[7] = 0;
 break; case   1: result[0] = 0; ... result[7] = 1;
 ...
 break; case 255: result[0] = 1; ... result[7] = 1;
}

这是冗长的代码,但您可以让预处理器帮助您编写它。

这可能更快的原因是开关应该变成跳跃 table,并且移动应该优化得很好。

添加:如果您想知道预处理器如何提供帮助,这里有一些东西:

#define FOO(x,i) result[i] = !!((x) & (1<<(i)))
#define BAR(x) break; case x: FOO(x,0);FOO(x,1); ... FOO(x,7)
switch(theChar){
 BAR(0);
 BAR(1);
 ...
 BAR(255);
}