GCC 出于什么目的创建应用于移位项的单独位掩码?

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再执行掩码是否有一些优势,即使是以在代码中存储额外常量为代价?
  • 如果没有,是否有一些 hack 或编译器标志我可以用来规避这个问题,并强制 GCCand首先简单地然后移动,以避免存储这些常量?

这是“编译器将优化代码,只需忘记它”的情况之一。并没有真正起作用,因为这种“优化”本身就是我认为有问题的。

我知道 16 字节的操作码“不多”,但我仍然很好奇为什么 GCC 会执行这种“优化”,尽管对于未经训练的人来说似乎是一种损失。即使是激进的尺寸优化也会发生这种情况。

Aki*_*nen 8

我只能推测 G​​CC 代码生成器被简单地编程为始终计算相对于最终位置的位掩码,即使这意味着位掩码的数量正在增长。

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,因为它会累积在比必要的更长的依赖链中。