【问题标题】:How do I efficiently compute reflections and rotations that map the unit cube onto itself?如何有效地计算将单位立方体映射到自身的反射和旋转?
【发布时间】:2020-05-22 20:49:44
【问题描述】:

对于基于八叉树的稀疏体素八叉树渲染器,我希望能够旋转和镜像八叉树的各个节点。虽然它不是真正的八叉树,但由于节点被共享并被允许将自己包含为子树。否则我可以简单地将转换应用于八叉树本身。

不失一般性,假设立方体是一个单位立方体,一个角在原点,即 (0,0,0),对角在 (1,1,1)。我已将立方体的角编码为 3 位整数。所以 0 (=0b000) 代表原点处的角点,1 (=0b001) 代表 (0,0,1) 处的角点,7 (=0b111) 代表 (1,1,1) 处的角点。唯一允许的旋转和反射是围绕立方体的中心 (½,½,½) 并且角将最终位于整数坐标处。这意味着只有 48 种不同的可能转换。 (第一个角可以映射到 8 个可能的位置,下一个角可以映射到 3,第三个可以映射到 2,这会固定其余角的位置。)

我还没有决定如何对转换进行编码,尽管应该可以将其编码为单个 32 位整数(甚至是 6 位整数,因为只有 48 种可能的转换)。然后可以将转换作为函数应用,该函数将立方体的每个角映射到转换后的位置。即

int transform(int corner, int transformation) {
  // magic happens here
  return result;
}

此外,我还应该有一个函数combine,它结合了转换,使得transform(corner, combine(a,b)) 等于transform(transform(corner, b), a)

因为这些函数每秒会被调用十亿次,所以它们应该很快。虽然 transform 的调用频率大约是 combine 的 4 倍。该算法是递归的,因此如果转换编码使用多个 32 位整数,则将其放入堆栈会产生额外的运行时成本。

到目前为止,我已经发现问题可以分解为位翻转,这可以通过单个异或操作和位排列(我还不知道如何有效地完成)来完成。不过,同时执行这两种操作的操作可能会更有效。

我打算在已经使用 SSE4.1 内在函数的 C++ 代码中使用它。尽管首选不使用内在函数或仅需要 SSE3 的解决方案。最后,速度是最重要的。我会尝试使用http://quick-bench.com/ 来比较解决方案。

(注意:我使用了仿射变换标签,因为没有正交变换标签,我不想创建它)。

【问题讨论】:

  • 你可以只连接 8 个新角并得到一个 24 位整数。
  • 然后我可以用一个简单的位移+掩码实现变换,这真的很快。虽然我将如何实现组合功能?
  • 如果你有两个转换 ab 这样a_ii-th 的图像(i 只是由二进制表示表示的自然数角)角,你可以通过设置c_i = b_{|a_i|}来计算它们的组合c,其中|a_i|是二进制字符串a_i表示的自然数。
  • 有一个很好的 6 位编码:首先将对称性分解为它在坐标平面 (x,y,z) 上的作用(六种可能性)加上它发送角的位置 (1,1, 1)。根据移位的索引是否以正确的循环顺序出现,将 (x,y,z) 的排列分解为符号位,然后用两位来描述 x 的去向。 (类似的技巧非常适合正方形的对称性......我不确定如何有效地处理三个元素的排列 - 也许只是使用表格查找?)

标签: c++ sse affinetransform isomorphism


【解决方案1】:

您可以将转换存储为 8 个字节的数组,该数组对立方体角的排列进行编码。 transform 方法就像数组查找一样简单。组合其中的两个转换可以在一个循环中完成,但这会相当慢。然而,事实证明 SSSE3 有一个内在函数可以在一条指令中执行此操作:_mm_shuffle_pi8

因此,必要的方法可以实现如下:

#include <tmmintrin.h>

int transform(int corner, __m64 transformation) {
    uint8_t array[8];
    memcpy(array, &transformation, 8); // This call is optimized away by the compiler.
    return array[corner];
}

__m64 combine(T a, T b) {
    return _mm_shuffle_pi8(b, a);
}

从我的基准测试结果中,我得到了四次调用 transform 并合并一次,花费了大约 2ns 的 CPU 时间。这仍然有点慢,但如果有足够的内核,它可能是可行的。


我还实现了一些不同的比较方法:

  • 将排列打包为 int32 中的半字节(4 位整数),并使用循环计算 combine 的结果。虽然使用了一半的内存,但速度却慢了大约 3-5 倍。
  • 直接使用 Milo Brandt 提出的 6 位编码。但是,我未能正确实施。
  • 将允许的转换限制为仅轴对齐反射的组合。这些可以使用按位异或快速计算。这大约快 30-50%。

我创建了一个quick-bench page,虽然它不支持使用SSSE3 指令,所以我不得不禁用使用_mm_shuffle_pi8 的方法。不过,您仍然可以在本地编译和运行它们。

【讨论】:

  • __m64 - 通常你不想使用 MMX。使用可以进入 XMM 寄存器的 __m128i 向量的低 8 字节同样快(或更快)。 _mm_storel_epi64_mm_loadl_epi64movq 存储/加载。
  • 另外,看起来transform 正在从向量元素中提取一个字节,然后将其零扩展回__m64 向量。你可以用 64 位位移和 AND 来做同样的事情,用 psrlq xmm,xmm (_mm_srl_epi64(v, count*8)) 将一个字节带到 64 位整数元素的底部
  • re:您的编辑:如果您希望结果为 int,在 x86-64 上,您最好的选择可能是 __int64 _mm_cvtsi128_si64x(__m128i a)。或者 _mm_cvtsi64_m64 如果您坚持使用 MMX。使用(integer &gt;&gt; (8*corner)) &amp; 0xFF。这将比使用索引寻址模式的movq store/movzx 重新加载具有更低的延迟,但会花费更多的前端微指令。
  • 如果 QuickBench 不能使用 SSSE3 的唯一原因是因为它不允许你通过 -march=skylake,你可以在文件顶部使用 #pragma GCC target("ssse3"),这样你就可以使用 SSSE3内在因素。 gcc.gnu.org/onlinedocs/gcc/…。虽然根据gcc target for AVX2 disabling SSE instruction set 可能实际上不起作用。
  • @Peter 在我的基准测试中,使用__m64__m128i 稍微快一点。虽然可能是我执行错误或编译器正在做一些优化。 W.r.t.转换,__m64 是一个错字,应该是int。使用(integer &gt;&gt; (8*corner)) &amp; 0xFF 似乎会导致严重的性能损失,因为它慢了大约 40-100%。我猜编译器在如何优化移动单个字节方面真的很聪明。
猜你喜欢
  • 1970-01-01
  • 2021-03-31
  • 1970-01-01
  • 2015-08-26
  • 1970-01-01
  • 2011-12-08
  • 1970-01-01
  • 2021-07-23
  • 1970-01-01
相关资源
最近更新 更多