use*_*248 6 c assembly gcc compiler-optimization constantfolding
以下是一个最小的可重现代码示例,我必须uint_fast64_t在给定的八叉树分支x z和y位置中生成 3D 坐标的“数组”(其 1 字节元素被打包到结果中):
#include <stdint.h>
void test(uint_fast64_t *const coord, const uint_fast8_t x, const uint_fast8_t z, const uint_fast8_t y) {
static const uint_fast64_t m = 0x2040810204081ULL, a = 0x101010101010101ULL;
*coord = (x * m & a) | (z * m & a) << 1 | (y * m & a) << 2;
}
Run Code Online (Sandbox Code Playgroud)
从汇编来看,GCC 似乎只生成常量的一个“变体” m,但生成了三个variants常量a,包括0x404040404040404和0x202020202020202。
test:
movabs rax, 567382630219905 ; 0x2040810204081
movzx edx, dl
movzx esi, sil
movzx ecx, cl
movabs r8, 144680345676153346 ; 0x202020202020202
imul rdx, rax
imul rsi, rax
imul rcx, rax
movabs rax, 289360691352306692 ; 0x404040404040404
add rdx, rdx
and rdx, r8
movabs r8, 72340172838076673 ; 0x101010101010101
and rsi, r8
sal rcx, 2
or rdx, rsi
and rcx, rax
or rdx, rcx
mov QWORD PTR [rdi], rdx
ret
Run Code Online (Sandbox Code Playgroud)
无论出于何种原因,GCC 似乎都在“不断地传播”<< 1和<< 2到这些掩码,并将它们单独存储,而同一个掩码只能通过and先进行位移再进行位移来使用。这就是令人困惑的地方。
另一方面,Clang<< 1将位移完全传播到常量,因此程序集包含 6 个 64 位常量,但没有与和相对应的移位操作<< 2。这似乎是以尺寸为代价的速度优化。
但我对海湾合作委员会的处理感到困惑。有些常量是“折叠”的,但有些则不是,而且它们没有提供任何明显的好处。
我的问题是:
and再执行掩码是否有一些优势,即使是以在代码中存储额外常量为代价?and首先简单地然后移动,以避免存储这些常量?这是“编译器将优化代码,只需忘记它”的情况之一。并没有真正起作用,因为这种“优化”本身就是我认为有问题的。
我知道 16 字节的操作码“不多”,但我仍然很好奇为什么 GCC 会执行这种“优化”,尽管对于未经训练的人来说似乎是一种损失。即使是激进的尺寸优化也会发生这种情况。
我只能推测 GCC 代码生成器被简单地编程为始终计算相对于最终位置的位掩码,即使这意味着位掩码的数量正在增长。
GCC 还有其他启发式方法,例如与不等式进行比较时将立即数减少 1。if (a < 2)转换为if (a <= 1),如果还需要计算if (a == 2)用于其他用途,则这是没有意义的。
无论如何,我们可以通过优化屏障来阻止 GCC 和 clang 进行一些优化asm("" :"+r"(a))——结合将常量作为非常量变量。
屏障通知包含的寄存器a正在被语句以某种方式修改asm,这意味着a不再包含常量。随后a << 1, a << 2也不再可以从 派生立即数a。
void test(uint_fast64_t *const coord, const uint_fast8_t x, const uint_fast8_t z, const uint_fast8_t y) {
uint_fast64_t m = 0x2040810204081ULL, a = 0x101010101010101ULL;
asm("" : "+r"(a));
uint_fast64_t xm = x * m & a;
uint_fast64_t ym = y * m & a;
uint_fast64_t zm = z * m & a;
*coord = xm | (zm << 1) | (ym << 2);
}
Run Code Online (Sandbox Code Playgroud)
在这种特殊情况下,人们显然也可以使用
void test(uint_fast64_t *const coord, const uint_fast8_t x, const uint_fast8_t z, const uint_fast8_t y) {
static const uint_fast64_t m = 0x2040810204081ULL, a = 0x101010101010101ULL;
*coord = (x * m & a) + (z * m & a) * 2 + (y * m & a) * 4;
}
Run Code Online (Sandbox Code Playgroud)
为了
test:
movabs r8, 567382630219905
movzx ecx, cl
movzx edx, dl
movabs rax, 72340172838076673
imul rcx, r8
movzx esi, sil
imul rdx, r8
imul rsi, r8
and rcx, rax
add rcx, rcx
and rdx, rax
add rcx, rdx
and rsi, rax
add rcx, rcx
add rcx, rsi
mov QWORD PTR [rdi], rcx
ret
Run Code Online (Sandbox Code Playgroud)
在这种情况下,我实际上期望lea rax, [rax + 4*rbx]使用格式,而不是两个单独的add rcx, rcx左移 1,因为它会累积在比必要的更长的依赖链中。