The following是我必须在给定xzy位置的八叉树分支内生成3D坐标的‘数组’(其1字节元素被打包到结果uint_fast64_t中)的最小可重现代码示例:

#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;
}

看一下汇编,GCC似乎只生成m常量的一个"变体",但生成a常量的三个variants,包括0x4040404040404040x202020202020202.

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

无论出于什么原因,GCC似乎一直在将第<< 1和第<< 2位传播到这些掩码上,并将它们分开存储,而同一个掩码只需先进行and位移位就可以使用.这就是令人困惑的地方.

另一方面,Clang将位移位完全传播到常量,因此程序集包含64位常量中的6个,但不包含与<< 1<< 2对应的移位操作.这似乎是以大小为代价的速度优化.

但我对GCC的处理方式感到困惑.一些常量是‘折叠’的,而另一些则不是,以及它们没有提供任何可察觉的好处的方式.

我的问题是:

  • 出于某种模糊的原因,先执行移位,然后再执行and掩码,即使是以在代码中存储额外常量为代价,也有一些好处吗?
  • 如果没有,有没有什么黑客或编译器标志可以用来绕过这一点,并迫使GCC首先简单地将其设置为and,然后进行移位,以避免存储这些常量?

这是一种"编译器会优化代码,忘了它"的情况.并不是真的起作用,因为我觉得这个‘优化’本身是有问题的.

我知道16字节的操作码"不多",但我仍然很好奇,为什么GCC会进行这种"优化",尽管看起来像是输给了一个外行人的眼睛.这甚至会发生在aggressive size optimizations岁的人身上.

推荐答案

我只能推测,GCC代码生成器的编程只是简单地相对于最终位置计算位掩码,即使这意味着位掩码的数量在增加.

GCC还有其他的启发式,比如与不等式相比,直接式减1.if (a < 2)被转换为if (a <= 1),如果还需要计算if (a == 2)以用于其他用途,这是没有意义的.


无论如何,可以通过优化屏障asm("" :"+r"(a))来防止GCC和clang进行某些优化--结合将常量作为非常量变量.

障碍通知包含a的寄存器正在被asm语句修改为somehow,这意味着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);
}

在这种特殊情况下,显然还可以使用

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;
}

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

In this case I would have actually expected lea rax, [rax + 4*rbx] 为mat to be used, instead of two separate add rcx, rcx to left-shift by 1 as it accumulates in a longer dependency chain than necessary.

C++相关问答推荐

如何在C中的空指针函数中传递浮点值

传递给空闲的无效地址0x71 db7 cb5e0:未分配值

设计处理各种数据类型的方法和数据 struct

通过MQTT/蚊子发送大文件—限制在4MB

字符串令牌化xpath表达式

如何判断宏参数是否为C语言中的整型文字

如何将已分配的数组(运行时已知的大小)放入 struct 中?

使用NameSurname扫描到两个单独的字符串

自定义应用程序上的日志(log)轮换问题

将指针作为参数传递给函数

CSAPP微型shell 实验室:卡在sigprocmask

是否可以使用指针算法在不对齐的情况下在 struct 中相同类型的字段的连续序列之间移动?

是否可以通过调用两个函数来初始化2D数组?示例:ARRAY[STARTING_ROWS()][STARTING_COLUMNS()]

C语言中浮点数的取整方式浮点数尾数超过23位时如何取整剩余部分

如何在VSCode中创建和使用我自己的C库?

Fscanf打印除退出C代码为1的程序外的所有内容

表达式x&;&;(~x)应该返回1还是0?它依赖于编译器吗?

C 程序不显示任何输出,但它接受 CS50 Lab1 的输入问题

如何用用户输入的多个字符串填充数组?

使用共享变量同步多线程 C 中的函数