【问题标题】:How can I get GCC to optimize this bit-shifting instruction into a move?如何让 GCC 将此位移指令优化为移动?
【发布时间】:2020-03-07 17:55:51
【问题描述】:

我正在尝试使用以下代码在软件中模拟 16 位半浮点数:

typedef struct half
{
    unsigned short mantissa:10;
    unsigned short exponent:5;
    unsigned short sign:1;
} half;

unsigned short from_half(half h)
{
    return h.mantissa | h.exponent << 10 | h.sign << 15;
}

half to_half(unsigned short s)
{
    half result = { s, s >> 10, s >> 15 };
    return result;
}

我对此进行了设置,以便可以轻松地将其优化为移动指令,但是你瞧,在 from_half 中,GCC 无论如何都会进行位移(即使在 -O3 处):

from_half:
        mov     edx, edi
        mov     eax, edi
        and     di, 1023
        shr     dx, 15
        and     eax, 31744
        movzx   edx, dl
        sal     edx, 15
        or      eax, edx
        or      eax, edi
        ret

虽然to_half 优化得很好:

to_half:
        mov     eax, edi
        ret

Godbolt

我尝试了不同的优化级别(-O1-O2-Os),但没有一个优化到我希望的水平。

即使在-O1,Clang 也会按照我的预期:

from_half:                              # @from_half
        mov     eax, edi
        ret
to_half:                                # @to_half
        mov     eax, edi
        ret

Godbolt

我怎样才能让 GCC 把它优化成一个动作?为什么还没有这样优化?

【问题讨论】:

  • 即使对于 to_half,优化在 gcc 中也发生得很晚。您可以在 gcc 的 bugzilla 中查找与位域相关的错误。
  • @MarcGlisse 看了看,没有一个开放的相似。
  • 请打开一个然后...
  • 需要注意的一点:“位”字段没有定义的顺序。例如,该单符号位可能是位 0 或可能是位 15。实际顺序取决于实现

标签: c gcc compiler-optimization bit-fields


【解决方案1】:

除了Booboo's answer,您还可以尝试以下回答您的问题的方法

如何让 GCC 将其优化为移动?

只需将每个移位的位域表达式转换为unsigned short

unsigned short from_half(half h)
{
    return (unsigned short)h.mantissa | (unsigned short)(h.exponent << 10) | (unsigned short)(h.sign << 15);
}

https://godbolt.org/z/CfZSgC

结果:

from_half:
        mov     eax, edi
        ret

为什么还没有这样优化?

我不确定我对此是否有可靠的答案。显然,位域到int 的中间提升使优化器感到困惑......但这只是一个猜测。

【讨论】:

  • 谢谢。这些演员阵容很丑陋,但它们确实产生了预期的结果。请参阅我的bug report 了解更新(还没有)。
  • @S.S.Anne 我希望他们修复它并且不会因为不是错误而关闭。
  • 他们不会。通常,树优化被认真对待。不过,他们可能需要一段时间。
【解决方案2】:

我已经有一段时间没有使用C 编码了,但似乎使用union 应该可以工作:

#include <stdint.h>
#include <stdbool.h>
#include <stdio.h>

static bool useUnion;

__attribute__ ((__constructor__)) // supported by gcc compiler
static void initUseUnion()
{
    union {
       uint16_t i;
       char c[2];
    } n = { 0x0001 };
    useUnion = n.c[0]; // little endian
}

typedef struct half
{
    unsigned short mantissa:10;
    unsigned short exponent:5;
    unsigned short sign:1;
} half;

typedef union half_short
{
    half h;
    uint16_t s;
} half_short;

unsigned short from_half(half h)
{
    if (useUnion) {
        half_short hs;
        hs.h = h;
        return hs.s;
    }
    else {
        return h.mantissa | h.exponent << 10 | h.sign << 15;
    }
}

half to_half(unsigned short s)
{
    if (useUnion) {
        half_short hs;
        hs.s = s;
        return hs.h;
    }
    else {
        half result = { s, s >> 10, s >> 15 };
        return result;
    }
}

int main(int argc, char* argv[])
{
    printf("%d\n", useUnion);
    return 0;
}

【讨论】:

  • 这充其量是不可移植的。如果我要用union 打双关,我不妨使用memcpy。请注意,在问题中我不依赖于字段的顺序。
  • 你当然是对的。但是您不认为to_half 被您的编译器很好地优化了仅仅是因为在您的特定实现中字段的顺序与short 的字节顺序完全一致吗?也许你希望或依赖太多。冒着让自己陷入困境的风险,我在本地修改了我的答案,以测试初始化​​代码中的大/小端序,以设置一个 useUnion 变量,可以测试该变量以确定是否应该使用联合。是否值得更新答案(联合比使用 memcpy 更好)?
  • 我关于“依赖太多”的观点暗示不同平台的大/小“字节序”可能会极大地阻碍您以便携方式为所有目标执行此操作的能力。
  • 是的。我必须找到更好的方法来做到这一点。不过,我仍然想知道为什么 GCC 会错过它。
  • 答案中发布的代码导致gcc 编译器输出几条关于intunsigned int 之间转换的消息,以及关于main() 的返回类型不是'int`跨度>
猜你喜欢
  • 2018-08-04
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2015-08-09
  • 1970-01-01
  • 1970-01-01
  • 2017-06-03
相关资源
最近更新 更多