【发布时间】: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