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

Fastest way to unpack bits into single precision floats

本文关键字:方法 精度 解压缩 单精度      更新时间:2023-10-16

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

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

我的平台是在支持AVX(但没有AVX2)的CPU上运行的Windows 64位。编译器:Visual Studio 2013。

谢谢。

预处理不是更快吗?2^8的可能性相当大,但话说回来,把它分成两部分,只有2^4=16个变量。

使数组由16个"值"组成,其中每个值都是用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个可能的值)复制值到哪里,这将非常快。

也不要自己写那么多代码,只需制作一个简单的程序,为您打印预处理值,复制并粘贴到原始程序中:)

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

无论如何,我们的想法是在每个双字中放置一个字节副本,并在每个通道中与正确的位进行AND运算,然后进行比较以形成掩码。最后,我们可以做一个逐位AND,将掩码转换为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]

CCD_ 7只复制了8次。

我不知道这是否更快,但值得一试。此外,这些都没有经过测试。

我试图将其转换为内部函数,但我不知道自己在做什么(而且它没有经过测试)。此外,要小心它使用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),我不确定这个想法有多好(这意味着它必须在内存中反弹int)。

有了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);
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;
}

在Intel和AMD的x86-64体系结构中,分支预测可以通过使用条件移动操作来执行(cmove):源操作数有条件地移动到目标取决于标志寄存器值的操作数。

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

正如@RippleR所建议的,索引也是我的第一个猜测。

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

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;
}

这是一个冗长的代码,但你可以让预处理器来帮助你写

这可能更快的原因是切换应该变成一个跳转表,并且移动应该优化得很好。

补充:如果你想知道预处理器如何帮助你,这里有一些东西:

#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);
}