【问题标题】:mod (%) in AVR assembly - __divmodhi4AVR 程序集中的 mod (%) - __divmodhi4
【发布时间】:2019-08-07 08:45:37
【问题描述】:

我正在尝试在 AVR 程序集中执行 %10。

我创建了一个简单的c文件

int main()
{
  int k=19;
  int j;
  j = k%10;
  return 0;
}

然后我将其编译成程序集,给出

    ldi r24,lo8(19)
    ldi r25,0
    std Y+2,r25
    std Y+1,r24
    ldd r24,Y+1
    ldd r25,Y+2
    ldi r18,lo8(10)
    ldi r19,0
    mov r22,r18
    mov r23,r19
    rcall __divmodhi4
    std Y+4,r25
    std Y+3,r24
    ldi r24,0
    ldi r25,0

__divmodhi4 是如何工作的,结果存储在哪里?

【问题讨论】:

  • 对于除以编译时常数,乘以定点逆并移动高半部分通常更有效。但是,GCC 仅在硬件划分足够宽以在一条乘法指令中执行此操作时才选择这样做。有关 AVR 和 x86-64 GCC 上的 unsigned short foo(unsigned short k) { return k%10;} 的 asm,请参见 godbolt.org/z/jOV_b4-O3。 (无符号除法比有符号除法更简单,因为例如-11 % 10 = -1,因此需要额外的移位才能正确处理符号。)通常查看 asm 以获取接受 args 并返回的函数。
  • @PeterCordes 正如您的 XKCD 卡通所暗示的那样,您是正确的。已移除。 :)

标签: assembly gcc avr modulo integer-division


【解决方案1】:

由于 AVR 没有硬件除法器,AVR-GCC 编译器必须使用复杂的函数来执行此类操作。

__divmodhi4 - 这些功能之一。它将存储在 r25:r24 中的有符号 16 位整数除以 r23:r22 中的另一个有符号 16 位整数。

在 r23:r22 中返回 16 位商,在 r25:r24 中返回余数

您应该在看到您自己的代码的同一反汇编中看到 __divmodhi4。

您还可以查看 GCC 库 for example, here 的查找源代码

【讨论】:

    【解决方案2】:

    为了自己理解这个函数是如何工作的,我用c写了一个版本。

    (如果你有办法在你的开发机器上逐步通过 AVR 汇编器,那么这可能是不必要的)

    这是一个有点直接的翻译:

        uint16_t udivmodhi4(uint16_t arg1, uint16_t arg2) {
            
            uint16_t rem = 0;
            
            uint8_t i = 16;
            uint8_t carry = 0;
            uint8_t carry2 = 0;
            
            do {
                carry2 = (arg1 & 0x8000) != 0;
                arg1 = (arg1 << 1) + carry;
                i--;
            
                rem = (rem << 1) + carry2;
                carry = arg2 > rem;
                if (!carry) {
                     rem = rem - arg2;
                }
            }
            while (i);
            
            arg1 = (arg1 << 1) + carry;
            
            arg1 = arg1 ^ 0xffff;
        
            // arg1 has the quotient, rem has the remainder
            return arg1;
            //return rem;
        }
    

    这是我的清理版本:

    uint16_t udivmodhi4(uint16_t arg1, uint16_t arg2) {
        uint16_t rem = 0;
    
        for (uint8_t i = 0; i < 16; i++) {
            rem = (rem << 1) | (arg1 & 0x8000 ? 1 : 0);
            arg1 = arg1 << 1;
            if (rem >= arg2) {
                rem -= arg2;
                arg1 |= 1;
            }
        }
        return arg1;
        //return rem;
    }
    

    如您所见,它循环了 16 次*,在每个循环中,它从 arg1 中取出最高位,将其移入余数的最低位,比较余数 arg2,然后将其移回 arg1,从中减去 arg2如有必要,其余部分。

    *:注意 ASM 在开始时将循环变量设置为 17,但在开始循环之前将其递减,因此循环了 16 次。此外,ASM 版本将返回到 arg1 的位反转,然后在最后翻转它们。代码中大多数类似这样的奇怪之处似乎是为了优化代码大小。

    c 代码不会像 ASM 那样优化到尽可能少的指令,我这样做只是为了学习。底线是,这会在 16 的循环中对任何被除数和除数进行 16 位无符号除法。

    【讨论】:

    • 相关:njuffa 在How can I multiply and divide using only bit shifting and adding? 上有一个答案,显示了通过在纯 C 中移位的逐位除法,并分别使用 x86 的内联 asm。看起来与清理后的版本非常相似,但rem 的初始化程序不涉及(arg1 &amp; 0x8000 ? 1 : 0)。 (您可以简化为 arg1 &gt;&gt; 15,尽管 IDK 如果任何一种方式更有可能帮助 GCC 看到它应该通过 adc rem,rem 从 arg1 的顶部移动到 rem 的底部)
    • 啊,谢谢你的链接,如果我注意到了,我就省去了麻烦。简洁的算法。
    • 编译器看起来对 (arg1 >> 15) 或 (arg1 >= 0x8000) 或 (arg1 & 0x8000 ? 1 : 0) 的所有变体进行了同等优化,也就是说,不是很好.将其转化为带进位的转变还不够聪明。看起来基本上需要 ASM 才能成为优化的解决方案。不过,这是一个很好的学习机会。
    猜你喜欢
    • 1970-01-01
    • 2016-09-15
    • 1970-01-01
    • 2014-06-27
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2017-09-27
    • 2011-09-30
    相关资源
    最近更新 更多