【问题标题】:"Anomaly" in signed integer in CC中带符号整数的“异常”
【发布时间】:2019-07-30 04:22:19
【问题描述】:

我目前正在写一篇关于 ARM 优化的讲座,特别是关于 NEON 等向量机作为最终目标。

而且由于向量机在 if-else 激流回旋中表现不佳,我正在尝试演示如何通过 bit-hacking 来摆脱它们。

我选择了“饱和绝对”函数作为示例。它实际上是一个 ABS 例程,具有将结果限制在 0x7fffffff 的附加功能。

最大可能的负 32 位数字是 0x80000000,这是一个非常危险的事情,因为 val = -val; 返回与初始值相同的 0x80000000,这是由于 two's complement 系统中的不对称性引起的,尤其是对于 DSP 操作,因此,它必须被过滤掉,主要是通过“饱和”。

int32_t satAbs1(int32_t val)
{
    if (val < 0) val = -val;
    if (val < 0) val = 0x7fffffff;
    return val;
}

下面是我在汇编中写的:

cmp     r0, #0
rsblts  r0, r0, #0
mvnlt   r0, #0x80000000
bx      lr

下面是我从上面的 C 代码中实际得到的:

satAbs1
        0x00000000:    CMP      r0,#0
        0x00000004:    RSBLT    r0,r0,#0
        0x00000008:    BX       lr

WTH?编译器完全丢弃了饱和部分!

编译器似乎排除了 val 在第一个 if 语句之后是否为负,如果它是 0x80000000 则不正确

或者函数应该返回一个无符号值?

uint32_t satAbs2(int32_t val)
{
    uint32_t result;
    if (val < 0) result = (uint32_t) -val; else result = (uint32_t) val;
    if (result == 0x80000000) result = 0x7fffffff;
    return result;
}

satAbs2
        0x0000000C:    CMP      r0,#0
        0x00000010:    RSBLT    r0,r0,#0
        0x00000014:    BX       lr

不幸的是,它生成的机器代码与签名版本完全相同:没有饱和。

同样,编译器似乎排除了val为0x80000000的情况

好的,让我们扩大第二个 if 语句的范围:

uint32_t satAbs3(int32_t val)
{
    uint32_t result;
    if (val < 0) result = (uint32_t) -val; else result = (uint32_t) val;
    if (result >= 0x80000000) result = 0x7fffffff;
    return result;
}

satAbs3
        0x00000018:    CMP      r0,#0
        0x0000001C:    RSBLT    r0,r0,#0
        0x00000020:    CMP      r0,#0
        0x00000024:    MVNLT    r0,#0x80000000
        0x00000028:    BX       lr

最后,编译器似乎正在完成它的工作,尽管它是最优的(与汇编版本相比,这是一个不必要的 CMP

我可以忍受编译器的次优,但困扰我的是他们排除了一些他们不应该做的事情:0x80000000

我什至会就此向GCC devs 提交错误报告,但我发现Clang 也排除了整数为 0x80000000 的情况,因此我想我遗漏了一些关于C 标准。

谁能告诉我哪里弄错了?

顺便说一句,下面是 if-less bit-hacking 版本的样子:

int32_t satAbs_bh(int32_t val)
{
    int32_t temp = val ^ (val>>31);
    val = temp + (val>>31);
    val ^= val>>31;
    return val;
}

satAbs_bh
        0x0000002C:    EOR      r3,r0,r0,ASR #31
        0x00000030:    ADD      r0,r3,r0,ASR #31
        0x00000034:    EOR      r0,r0,r0,ASR #31
        0x00000038:    BX       lr

编辑:我同意我的这个问题在某种程度上是重复的。
但是,它更全面,包括一些汇编级别的东西和位掩码技术,与所提到的相比可能会有所帮助。

下面是解决此问题的方法,而无需破坏编译器选项;抢先排除整数溢出的可能性:

int32_t satAbs4(int32_t val)
{
    if (val == 0x80000000) return 0x7fffffff;
    if (val < 0) val = -val;
    return val;
}
satAbs4
        0x0000002C:    CMP      r0,#0x80000000
        0x00000030:    BEQ      {pc}+0x10 ; 0x40
        0x00000034:    CMP      r0,#0
        0x00000038:    RSBLT    r0,r0,#0
        0x0000003C:    BX       lr
        0x00000040:    MVN      r0,#0x80000000
        0x00000044:    BX       lr

我使用的linaro GCC 7.4.1 再次证明了它的缺点:我不理解第 2 行中的BEQ。源代码中建议的moveq r0, #0x80000001 本来可以在末尾保存两条指令。

【问题讨论】:

  • This answer 提到编译器可能会利用这样一个事实,即尝试做你正在做的事情是未定义的行为,并进行优化。
  • @pmg 我没有使用你提到的任何库。我写的是纯C,我根本无法认同编译器生成的机器码。
  • 对不起@Jake'Alquimista'LEE,但原因相同:UB。评论还是被删除了

标签: c optimization integer arm bit-manipulation


【解决方案1】:

有符号整数上溢或下溢在 C 中是未定义的行为,这意味着您需要自己处理这些边缘情况。换句话说,一旦编译器确定某个有符号整数值是正数,它就不会关心它是否有可能通过 UB 变为负数。

例如这段代码:

int test(int input)
{
    if (input > 0)
        input += 100;

    if (input > 0)
        input += 100;

    if (input > 0)
        input += 100;

    return input;
}

可以合法地是optimized to this:

int test(int input)
{
    if (input > 0)
        input += 300;

    return input;
}

即使初始代码的作者可能已经预料到input 可能会在每个连续语句之间溢出。

这就是优化编译器将您的代码视为如下内容的原因:

int32_t satAbs1(int32_t val)
{
    if (val < 0) val = -val;

    // val must be positive here,
    // unless you are relying on UB

    // the following condition is 
    // therefore always false:
    // if (val < 0) val = 0x7fffffff;

    return val;
}

因此,避免 UB 的唯一方法是避免在有符号整数有可能调用 UB 时取反,即:

int32_t satAbs3_simple(int32_t val)
{
    if (val >= 0) 
        return val;

    // we know that val is negative here, 
    // but unfortunately gcc knows it as well,
    // so we'll handle the edge case explicitly

    if (val == INT32_MIN)
        return INT32_MAX;

    return -val;
}

带有 -O2 的 gcc 生成带有分支的代码(在bxge 的早期条件返回):

satAbs3_basic:
  cmp r0, #0
  bxge lr // return r0 if ge #0
  cmp r0, #0x80000000
  rsbne r0, r0, #0
  moveq r0, #0x7FFFFFFF
  bx lr

正如@rici 在 cmets 中提到的那样,如果您的编译器上有来自 stdint.h (intN_t) 的精确宽度有符号 int 类型,这意味着它们必须用 N 位表示,没有填充,使用 2 的补码.

这意味着您可以稍微重写代码以使用位掩码,这可能会提供稍短的汇编输出(至少使用gcc 5 or newer),但仍然没有分支:

int32_t satAbs3_c(int32_t val)
{
    uint32_t result = (uint32_t)val;
    if (result & 0x80000000) result = -result; // <-- avoid UB here by negating uint32_t
    if (result == 0x80000000) result = 0x7FFFFFFF;
    return (int32_t)result;
}

请注意,优化编译器理论上应该能够为这两种情况产生相同的输出,但无论如何,最后一个 sn-p 的最新 gcc 版本(带有 -O1)给出:

satAbs3_c:
  cmp r0, #0
  rsblt r0, r0, #0
  cmp r0, #0x80000000
  moveq r0, #0x7FFFFFFF
  bx lr

我实际上相信它不能比这更短(除了 xor 位黑客),因为您的初始程序集似乎在 rsblts 之后缺少 cmp r0, #0 指令(因为 rsblts 更改了 r0 和 @ 987654340@是实际比较的部分)。

【讨论】:

  • @Jake'Alquimista'LEE:是的,有符号的 int 溢出与积极的编译器优化相结合常常让人感到惊讶。例如。 this answer on the ARM forum 在使用 gcc 编译并启用优化时不正确(您可以看到 here 在所有情况下都只使用 rsb 指令)。
  • 这很有趣。严格来说,我唯一的防水版本是satAbs4。即使satAbs_bh 可能根据定义是错误的。那么我应该如何优化 C 中的东西呢?多么愚蠢……
  • @Jake'Alquimista'LEE:我已经更新了答案,我相信在没有 UB 和位黑客攻击的情况下应该可以解决问题。右移负数是实现定义的,所以如果可能的话我也会避免它(并且转换为 unsigned 不会有相同的效果)。
  • 您的 satAbs3_c 不是实现定义的,因为它假设 2 的补码,因为它假设 int32_t 存在。 intN_t 是可选的(第 7.20.1.1/3 节),但如果存在,它是“宽度为 N、无填充位和二进制补码表示的有符号整数类型”。 (7.20.1.1/1)
猜你喜欢
  • 2022-05-07
  • 2012-02-11
  • 1970-01-01
  • 2018-02-26
  • 1970-01-01
  • 1970-01-01
  • 2011-03-15
  • 2015-01-24
  • 1970-01-01
相关资源
最近更新 更多