微信公众号搜"智元新知"关注
微信扫一扫可直接关注哦!

为什么 ARM gcc 在除以常数时调用 __udivsi3?

如何解决为什么 ARM gcc 在除以常数时调用 __udivsi3?

我使用的是 ARM 封装的 GCC 的最新可用版本:

arm-none-eabi-gcc(GNU Arm 嵌入式工具链 10-2020-q4-major)10.2.1 20201103(发布) 版权所有 (C) 2020 Free Software Foundation,Inc.

当我使用“-mcpu=cortex-m0 -mthumb -Ofast”编译这段代码时:

int main(void) {
    uint16_t num = (uint16_t) ADC1->DR;
    ADC1->DR = num / 7;
}

我希望通过乘法和移位来完成除法,但生成的是以下代码

 08000b5c <main>:
 8000b5c: b510 push {r4,lr}
 8000b5e: 4c05 ldr r4,[pc,#20] ; (8000b74 <main+0x18>)
 8000b60: 2107 movs r1,#7
 8000b62: 6c20 ldr r0,[r4,#64] ; 0x40
 8000b64: b280 uxth r0,r0
 8000b66: f7ff facf bl 8000108 <__udivsi3>
 8000b6a: b280 uxth r0,r0
 8000b6c: 6420 str r0,#64] ; 0x40
 8000b6e: 2000 movs r0,#0
 8000b70: bd10 pop {r4,pc}
 8000b72: 46c0 nop ; (mov r8,r8)
 8000b74: 40012400 .word 0x40012400

使用 __udivsi3 而不是乘法和移位是非常低效的。是我使用了错误的标志,还是遗漏了其他东西,或者这是 GCC 错误

解决方法

Cortex-M0 缺少执行 32x32->64 位乘法的指令。因为 num 是一个无符号的 16 位数量,将它乘以 9363 并右移 16 将在所有情况下产生正确的结果,但是——可能是因为 uint16_t 将在乘法之前提升为 int,gcc 不包括这样的优化。

从我观察到的情况来看,gcc 在针对 Cortex-M0 的优化方面做得很差,没有采用一些适合该平台的直接优化,但有时采用了不适合的“优化”。鉴于类似

void test1(uint8_t *p)
{
    for (int i=0; i<32; i++)
        p[i] = (p[i]*9363) >> 16; // Divide by 7
}

gcc 恰好在 -O2 处为 Cortex-M0 生成了正确的代码,但如果乘法被替换为加法,编译器将生成代码,在循环的每次迭代中重新加载常量 9363。使用加法时,即使代码改成:

void test2(uint16_t *p)
{
    register unsigned u9363 = 9363;
    for (int i=0; i<32; i++)
        p[i] = (p[i]+u9363) >> 16;
}

gcc 仍会将常量的负载带入循环。有时 gcc 的优化也可能会产生意想不到的行为后果。例如,人们可能期望在像 Cortex-M0 这样的平台上调用类似的东西:

unsigned short test(register unsigned short *p)
{
    register unsigned short temp = *p;
    return temp - (temp >> 15);
}    

当中断更改 *p 的内容时,可能会产生与旧值或新值一致的行为。标准不需要这样的处理,但大多数适用于嵌入式编程任务的实现将提供比标准要求的更强的保证。如果旧值或新值都可以接受,让编译器使用更方便的任何一个可能比使用 volatile 允许更高效的代码。然而,碰巧的是,来自 gcc 的“优化”代码将用单独的 temp 负载替换 *p 的两种用法。

如果您在 Cortex-M0 上使用 gcc 并且完全关心性能或“惊人”行为的可能性,请养成检查编译器输出的习惯。对于某些类型的循环,甚至可能值得考虑测试 -O0。如果代码适当地使用了 register 关键字,其性能有时会超过使用 -O2 处理的相同代码的性能。

,

扩展超级猫的答案。

喂这个:

unsigned short fun ( unsigned short x )
{
    return(x/7);
}

具有更大乘法的东西:

00000000 <fun>:
   0:   e59f1010    ldr r1,[pc,#16]   ; 18 <fun+0x18>
   4:   e0832190    umull   r2,r3,r0,r1
   8:   e0400003    sub r0,r3
   c:   e08300a0    add r0,lsr #1
  10:   e1a00120    lsr r0,#2
  14:   e12fff1e    bx  lr
  18:   24924925    .word   0x24924925
  

二进制的 1/7(长除法):

     0.001001001001001
 111)1.000000
       111 
      ==== 
         1000
          111
          ===
            1
            
        
0.001001001001001001001001001001
0.0010 0100 1001 0010 0100 1001 001001
0x2492492492...
0x24924925>>32  (rounded up)

为此,您需要一个 64 位的结果,取上半部分并进行一些调整,例如:

7 * 0x24924925 = 0x100000003

然后取前 32 位(不是那么简单,但是对于这个值,您可以看到它起作用)。

全拇指变体乘法是 32 位 = 32 位 * 32 位,因此结果将是 0x00000003,这不起作用。

所以是 0x24924,我们可以像 supercat 那样制作 0x2493 或 0x2492。

现在我们可以使用 32x32 = 32 位乘法:

0x2492 * 7 = 0x0FFFE
0x2493 * 7 = 0x10005

让我们用更大的来运行:

0x100000000/0x2493 = a number greater than 65536. so that is fine.

但是:

0x3335 * 0x2493 = 0x0750DB6F
0x3336 * 0x2493 = 0x07510002
0x3335 / 7 = 0x750
0x3336 / 7 = 0x750

所以你只能用这种方法走得更远。

如果我们按照arm代码的模型:

for(ra=0;ra<0x10000;ra++)
{
    rb=0x2493*ra;
    rd=rb>>16;
    rb=ra-rd;
    rb=rd+(rb>>1);
    rb>>=2;
    rc=ra/7;
    printf("0x%X 0x%X 0x%X \n",ra,rb,rc);
    if(rb!=rc) break;
}

然后它从 0x0000 到 0xFFFF,所以你可以编写 asm 来做到这一点(注意它需要是 0x2493 而不是 0x2492)。

如果您知道操作数不会超过某个值,那么您可以使用更多的 1/7 位进行乘法运算。

在任何情况下,当编译器没有为您进行这种优化时,您自己可能仍然有机会。

现在回想起来,我以前遇到过这个问题,现在它是有道理的。但是我在一个全尺寸的手臂上,我调用了一个我在手臂模式下编译的例程(另一个代码在拇指模式下),并且基本上有一个 switch 语句,如果分母 = 1 那么结果 = x/1;如果分母 = 2,则结果 = x/2,依此类推。然后它避免了 gcclib 函数并生成了 1/x 乘法。 (我想除以 3 或 4 个不同的常量):

unsigned short udiv7 ( unsigned short x )
{
    unsigned int r0;
    unsigned int r3;
    
    r0=x;
    r3=0x2493*r0;
    r3>>=16;
    r0=r0-r3;
    r0=r3+(r0>>1);
    r0>>=2;
    return(r0);
}

假设我没有犯错:

00000000 <udiv7>:
   0:   4b04        ldr r3,#16]   ; (14 <udiv7+0x14>)
   2:   4343        muls    r3,r0
   4:   0c1b        lsrs    r3,#16
   6:   1ac0        subs    r0,r3
   8:   0840        lsrs    r0,#1
   a:   18c0        adds    r0,r3
   c:   0883        lsrs    r3,#2
   e:   b298        uxth    r0,r3
  10:   4770        bx  lr
  12:   46c0        nop         ; (mov r8,r8)
  14:   00002493    .word   0x00002493

这应该比通用除法库例程更快。

编辑

我想我看到 supercat 用有效的解决方案做了什么:

((i*37449 + 16384u) >> 18)

我们把它作为 1/7 的分数:

0.001001001001001001001001001001

但我们只能进行 32 = 32x32 位乘法。前导零为我们提供了一些我们可以利用的喘息空间。因此,我们可以尝试使用 0x2492/0x2493 代替:

1001001001001001
0x9249
0x9249*0xFFFF = 0x92486db7

到目前为止它不会溢出:

    rb=((ra*0x9249) >> 18);

它本身在 7 * 0x9249 = 0x3FFFF 处失败,0x3FFFF>>18 是零而不是 1。

所以也许

    rb=((ra*0x924A) >> 18);

失败于:

    0xAAAD 0x1862 0x1861 

那又如何:

    rb=((ra*0x9249 + 0x8000) >> 18);

那行得通。

超级猫呢?

    rb=((ra*0x9249 + 0x4000) >> 18);

并且对于 0x0000 到 0xFFFF 的所有值都运行干净:

    rb=((ra*0x9249 + 0x2000) >> 18);

这里失败了:

0xE007 0x2000 0x2001 

所以有几个可行的解决方案。

unsigned short udiv7 ( unsigned short x )
{
    unsigned int ret;
    ret=x;
    ret=((ret*0x9249 + 0x4000) >> 18);
    return(ret);
}
00000000 <udiv7>:
   0:   4b03        ldr r3,#12]   ; (10 <udiv7+0x10>)
   2:   4358        muls    r0,r3
   4:   2380        movs    r3,#128    ; 0x80
   6:   01db        lsls    r3,#7
   8:   469c        mov ip,r3
   a:   4460        add r0,ip
   c:   0c80        lsrs    r0,#18
   e:   4770        bx  lr
  10:   00009249    .word   0x00009249

编辑

就“为什么”问题而言,这不是 Stack Overflow 的问题;如果您想知道为什么 gcc 不这样做,请询问该代码的作者。我们所能做的就是在这里进行推测,推测是他们可能选择不这样做是因为指令的数量,或者他们可能选择不这样做,因为他们有一个算法说明这不是 64 = 32x32 位乘法然后做不打扰。

同样,为什么问题不是堆栈溢出问题,所以也许我们应该关闭这个问题并删除所有答案。

我发现这非常具有教育意义(一旦您知道/理解了所说的内容)。

另一个为什么?问题是为什么 gcc 会按照他们的方式来做,而他们本可以按照 supercat 或我的方式来做呢?

,

编译器只能在知道结果对于语言允许的任何输入都是正确的情况下才能重新排列整数表达式。

因为 7 与 2 互质,所以不可能用乘法和移位来将任何输入除以 7。

如果您知道您打算提供的输入是可能的,那么您必须使用乘法和移位运算符自行完成。

根据输入的大小,您必须选择移动多少,以便输出正确(或至少对您的应用程序足够好)并且中间不会溢出。编译器无法知道什么对您的应用程序来说足够准确,或者您的最大输入是多少。如果它允许任何输入达到类型的最大值,那么每次乘法都会溢出。

一般情况下,GCC 只会在除数不是 2 的互质(即它是 2 的幂)时使用移位进行除法。

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。