如何有效地解交织位(逆莫顿)
-
29-10-2019 - |
题
这个问题: 如何去交织位(UnMortonizing?) 对于提取莫顿数的两半之一(仅奇数位)有一个很好的答案,但我需要一个解决方案,以尽可能少的操作提取两个部分(奇数位和偶数位)。
对于我的使用,我需要获取一个 32 位 int 并提取两个 16 位 int,其中一个是偶数位,另一个是右移 1 位的奇数位,例如
input, z: 11101101 01010111 11011011 01101110
output, x: 11100001 10110111 // odd bits shifted right by 1
y: 10111111 11011010 // even bits
似乎有很多解决方案使用带有幻数的移位和掩码来生成莫顿数(即交织位),例如 按二进制幻数交错位, ,但我还没有找到任何可以做相反的事情(即去交织)。
更新
在重新阅读《Hacker's Delight》中有关完美洗牌/不洗牌的部分后,我发现了一些有用的示例,我将其改编如下:
// morton1 - extract even bits
uint32_t morton1(uint32_t x)
{
x = x & 0x55555555;
x = (x | (x >> 1)) & 0x33333333;
x = (x | (x >> 2)) & 0x0F0F0F0F;
x = (x | (x >> 4)) & 0x00FF00FF;
x = (x | (x >> 8)) & 0x0000FFFF;
return x;
}
// morton2 - extract odd and even bits
void morton2(uint32_t *x, uint32_t *y, uint32_t z)
{
*x = morton1(z);
*y = morton1(z >> 1);
}
我认为这仍然可以改进,无论是当前的标量形式还是利用 SIMD,因此我仍然对更好的解决方案感兴趣(标量或 SIMD)。
解决方案
如果您的处理器有效地处理64位INT,则可以将操作组合在一起...
int64 w = (z &0xAAAAAAAA)<<31 | (z &0x55555555 )
w = (w | (w >> 1)) & 0x3333333333333333;
w = (w | (w >> 2)) & 0x0F0F0F0F0F0F0F0F;
...
. 其他提示
英特尔哈斯韦尔和稍后的CPU的代码。您可以使用包含PEXT和PDEP指令的BMI2指令集。这些可以(以及其他伟大的东西)用于构建您的功能。
#include <immintrin.h>
#include <stdint.h>
// on GCC, compile with option -mbmi2, requires Haswell or better.
uint64_t xy_to_morton (uint32_t x, uint32_t y)
{
return _pdep_u32(x, 0x55555555) | _pdep_u32(y,0xaaaaaaaa);
}
uint64_t morton_to_xy (uint64_t m, uint32_t *x, uint32_t *y)
{
*x = _pext_u64(m, 0x5555555555555555);
*y = _pext_u64(m, 0xaaaaaaaaaaaaaaaa);
}
. uint64_t morton3(uint64_t x) {
x = x & 0x9249249249249249;
x = (x | (x >> 2)) & 0x30c30c30c30c30c3;
x = (x | (x >> 4)) & 0xf00f00f00f00f00f;
x = (x | (x >> 8)) & 0x00ff0000ff0000ff;
x = (x | (x >> 16)) & 0xffff00000000ffff;
x = (x | (x >> 32)) & 0x00000000ffffffff;
return x;
}
uint64_t bits;
uint64_t x = morton3(bits)
uint64_t y = morton3(bits>>1)
uint64_t z = morton3(bits>>2)
. 如果您需要的速度比您可以使用表 - 查找一次字节转换(两个字节表更快,但大)。程序是在Delphi IDE下进行的,但汇编程序/ algorithem是相同的。
const
MortonTableLookup : array[byte] of byte = ($00, $01, $10, $11, $12, ... ;
procedure DeinterleaveBits(Input: cardinal);
//In: eax
//Out: dx = EvenBits; ax = OddBits;
asm
movzx ecx, al //Use 0th byte
mov dl, byte ptr[MortonTableLookup + ecx]
//
shr eax, 8
movzx ecx, ah //Use 2th byte
mov dh, byte ptr[MortonTableLookup + ecx]
//
shl edx, 16
movzx ecx, al //Use 1th byte
mov dl, byte ptr[MortonTableLookup + ecx]
//
shr eax, 8
movzx ecx, ah //Use 3th byte
mov dh, byte ptr[MortonTableLookup + ecx]
//
mov ecx, edx
and ecx, $F0F0F0F0
mov eax, ecx
rol eax, 12
or eax, ecx
rol edx, 4
and edx, $F0F0F0F0
mov ecx, edx
rol ecx, 12
or edx, ecx
end;
. 我不想局限于固定大小的整数并使用硬编码常量制作类似命令的列表,因此我开发了一个 C++11 解决方案,它利用模板元编程来生成函数和常量。生成的汇编代码 -O3
看起来在不使用 BMI 的情况下已经达到了极限:
andl $0x55555555, %eax
movl %eax, %ecx
shrl %ecx
orl %eax, %ecx
andl $0x33333333, %ecx
movl %ecx, %eax
shrl $2, %eax
orl %ecx, %eax
andl $0xF0F0F0F, %eax
movl %eax, %ecx
shrl $4, %ecx
orl %eax, %ecx
movzbl %cl, %esi
shrl $8, %ecx
andl $0xFF00, %ecx
orl %ecx, %esi
执行
基本上每一步 morton1
函数的工作原理是移位并添加到一系列常量中,如下所示:
0b0101010101010101
(交替 1 和 0)0b0011001100110011
(交替 2x 1 和 0)0b0000111100001111
(交替 4x 1 和 0)0b0000000011111111
(交替 8x 1 和 0)
如果我们要使用 D
尺寸,我们会有一个图案 D-1
零点和 1
一。因此,要生成这些,生成连续的并应用一些按位或就足够了:
/// @brief Generates 0b1...1 with @tparam n ones
template <class T, unsigned n>
using n_ones = std::integral_constant<T, (~static_cast<T>(0) >> (sizeof(T) * 8 - n))>;
/// @brief Performs `@tparam input | (@tparam input << @tparam width` @tparam repeat times.
template <class T, T input, unsigned width, unsigned repeat>
struct lshift_add :
public lshift_add<T, lshift_add<T, input, width, 1>::value, width, repeat - 1> {
};
/// @brief Specialization for 1 repetition, just does the shift-and-add operation.
template <class T, T input, unsigned width>
struct lshift_add<T, input, width, 1> : public std::integral_constant<T,
(input & n_ones<T, width>::value) | (input << (width < sizeof(T) * 8 ? width : 0))> {
};
现在我们可以使用以下命令在编译时生成任意维度的常量:
template <class T, unsigned step, unsigned dimensions = 2u>
using mask = lshift_add<T, n_ones<T, 1 << step>::value, dimensions * (1 << step), sizeof(T) * 8 / (2 << step)>;
使用相同类型的递归,我们可以为算法的每个步骤生成函数 x = (x | (x >> K)) & M
:
template <class T, unsigned step, unsigned dimensions>
struct deinterleave {
static T work(T input) {
input = deinterleave<T, step - 1, dimensions>::work(input);
return (input | (input >> ((dimensions - 1) * (1 << (step - 1))))) & mask<T, step, dimensions>::value;
}
};
// Omitted specialization for step 0, where there is just a bitwise and
仍然需要回答“我们需要多少步骤?”的问题。这还取决于维数。一般来说, k
步骤计算 2^k - 1
输出位;每个维度的最大有意义位数由下式给出 z = sizeof(T) * 8 / dimensions
, ,因此只需取 1 + log_2 z
脚步。现在的问题是我们需要这个 constexpr
以便将其用作模板参数。我发现解决这个问题的最好方法是定义 log2
通过元编程:
template <unsigned arg>
struct log2 : public std::integral_constant<unsigned, log2<(arg >> 1)>::value + 1> {};
template <>
struct log2<1u> : public std::integral_constant<unsigned, 0u> {};
/// @brief Helper constexpr which returns the number of steps needed to fully interleave a type @tparam T.
template <class T, unsigned dimensions>
using num_steps = std::integral_constant<unsigned, log2<sizeof(T) * 8 / dimensions>::value + 1>;
最后,我们可以执行一次调用:
/// @brief Helper function which combines @see deinterleave and @see num_steps into a single call.
template <class T, unsigned dimensions>
T deinterleave_first(T n) {
return deinterleave<T, num_steps<T, dimensions>::value - 1, dimensions>::work(n);
}