将位解压缩为单精度浮点数的最快方法
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.
循环、条件和遍历内存中的实际数组当然不是向量方式。所以这是另一个想法,尽管它在仅 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, 8
和16, 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);
}
这是平台特定的问题。速度至关重要。 将一个字节解包到一个由 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.
循环、条件和遍历内存中的实际数组当然不是向量方式。所以这是另一个想法,尽管它在仅 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, 8
和16, 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);
}