【问题标题】:Divide by 10 using bit shifts?使用位移除以 10?
【发布时间】:2011-07-30 07:58:37
【问题描述】:

是否可以通过使用纯位移、加法、减法和也许乘法来将无符号整数除以 10?使用资源非常有限且划分速度较慢的处理器。

【问题讨论】:

  • 有可能(重复减法是除法),但问题是它是否比慢除法更快。
  • @esnyder。对不起,我无法理解你。你说的是 17 基还是 22 基?
  • 基地大两个。右移除以 2^n 可以解决您的问题,如果“10”是指十进制数 16 或 10h。
  • 你在跟我吵架吗?我实际上是在试图承认 没有提到我的答案不是十进制的......可能有点晦涩,但这是我的意图。
  • @esynder,是的,我想我是在和你争论,把 10(以 10 为底)解释为 10(以 16 为底)。我认为默认情况下这样的解释是不寻常的,充其量是。

标签: math bit micro-optimization low-level integer-division


【解决方案1】:

除法就是减法,所以是的。右移 1(除以 2)。现在从结果中减去 5,计算减法的次数,直到值小于 5。结果是减法的次数。哦,除法可能会更快。

如果除法器中的逻辑尚未为您执行此操作,则右移然后使用正常除法除以 5 的混合策略可能会提高性能。

【讨论】:

    【解决方案2】:

    当然可以,如果您可以忍受一些精度损失。如果您知道输入值的值范围,则可以得出一个位移位和一个精确的乘法。 一些示例如何除以 10、60,... 就像在此博客中描述的一样,可以格式化 time the fastest way

    temp = (ms * 205) >> 11;  // 205/2048 is nearly the same as /10
    

    【讨论】:

    • 您必须注意中间值(ms * 205) 可能会溢出。
    • 如果你这样做 int ms = 205 * (i >> 11);如果数字很小,您将得到错误的值。您需要一个测试套件来确保在给定的值范围内结果是正确的。
    • 这对于 ms = 0..1028 是准确的
    • @ernesto >> 11 是 2048 的除法。当您想除以 10 时,您需要除以 2048/10,即 204,8 或 205 作为最接近的整数。
    • 对于 0 temp = (ms * 103) >> 10;
    【解决方案3】:

    编者注:这不是实际上编译器所做的,gives the wrong answer 用于以 9 结尾的大正整数,以 div10(1073741829) = 107374183 而不是 107374182 开头。不过,这对于较小的输入是准确的,这对于某些用途可能就足够了。

    编译器(包括 MSVC)确实对常数除数使用定点乘法逆运算,但它们使用不同的魔法常数并对高半结果进行移位以获得所有可能输入的精确结果,与 C 抽象机的结果相匹配需要。算法见Granlund & Montgomery's paper

    请参阅Why does GCC use multiplication by a strange number in implementing integer division?,了解实际的 x86 asm gcc、clang、MSVC、ICC 和其他现代编译器的示例。


    这是一个快速的近似值,对于大型输入是不精确的

    它甚至比编译器使用的乘法+右移精确除法还要快。

    您可以使用乘法结果的高半部分除以小的整数常数。假设是32位机器(代码可以做相应调整):

    int32_t div10(int32_t dividend)
    {
        int64_t invDivisor = 0x1999999A;
        return (int32_t) ((invDivisor * dividend) >> 32);
    }
    

    这里的情况是,我们乘以 1/10 * 2^32 的近似值,然后删除 2^32。这种方法可以适应不同的除数和不同的位宽。

    这对 ia32 架构非常有效,因为它的 IMUL 指令会将 64 位乘积放入 edx:eax,而 edx 值将是想要的值。即(假设在 eax 中传递除数,在 eax 中返回商)

    div10 proc 
        mov    edx,1999999Ah    ; load 1/10 * 2^32
        imul   eax              ; edx:eax = dividend / 10 * 2 ^32
        mov    eax,edx          ; eax = dividend / 10
        ret
        endp
    

    即使在具有较慢乘法指令的机器上,这也会比软件甚至硬件除法更快。

    【讨论】:

    • +1,我想强调一下,当你写“x/10”时,编译器会自动为你做这件事
    • 嗯,这里是不是有些数字不准确?
    • 在进行整数除法时总是会出现数值不准确:使用整数将 28 除以 10 时会得到什么?答案:2.
    • 整数除法没有数值不准确,结果是精确指定的。但是,上面的公式仅适用于某些除数。如果要进行无符号运算,即使 10 也不准确:4294967219 / 10 = 429496721,但 4294967219 * div >> 32 = 429496722 对于更大的除数,签名版本也将不准确。
    • @Theran:不,包括 MSVC 在内的编译器会将 x/10 编译为 a fixed-point multiplicative inverse(并编写额外的代码来处理带符号除法的负输入),以便为所有可能的 32 位输入提供正确答案.对于无符号除以 10,MSVC(和其他编译器)(godbolt.org/g/aAq7jx)将乘以 0xcccccccd,并将高半部分右移 3。
    【解决方案4】:

    虽然到目前为止给出的答案与实际问题相符,但与标题不符。所以这里有一个深受Hacker's Delight 启发的解决方案,它真正只使用位移位。

    unsigned divu10(unsigned n) {
        unsigned q, r;
        q = (n >> 1) + (n >> 2);
        q = q + (q >> 4);
        q = q + (q >> 8);
        q = q + (q >> 16);
        q = q >> 3;
        r = n - (((q << 2) + q) << 1);
        return q + (r > 9);
    }
    

    我认为这是缺少乘法指令的架构的最佳解决方案。

    【讨论】:

    • pdf 不再可用
    • 我们怎样才能让它适应 10^N?
    • 原始站点已失效,链接现在指向 Wayback Machine 中的存档版本。在链接的 PDF 中,您将找到除以 100 和 1000 的代码。请注意,这些代码仍然包含需要用移位和加法替换的乘法运算。此外,divu100 和 divu1000 代码包含许多不是 8 的倍数的移位,因此如果您使用的架构既没有桶形移位器也没有多重指令,那么重复应用 divu10 可能会更好。
    • 谢谢!这是针对 FPGA/RTL 的,我会根据我能得到的时间来调整。我刚刚找到了这个 pdf 的链接,到处都是这样的问题。无法找到实际文件。再次感谢!
    • 通常缺乏 MUL 的架构也缺乏对一次多位移位的支持,例如 AVR 8 位,这会导致各种移位的循环堆积如山
    【解决方案5】:

    在一次只能移动一个位置的架构上,与 2 乘以 10 的递减幂进行一系列明确的比较可能比黑客喜欢的解决方案更有效。假设一个 16 位的红利:

    uint16_t div10(uint16_t dividend) {
      uint16_t quotient = 0;
      #define div10_step(n) \
        do { if (dividend >= (n*10)) { quotient += n; dividend -= n*10; } } while (0)
      div10_step(0x1000);
      div10_step(0x0800);
      div10_step(0x0400);
      div10_step(0x0200);
      div10_step(0x0100);
      div10_step(0x0080);
      div10_step(0x0040);
      div10_step(0x0020);
      div10_step(0x0010);
      div10_step(0x0008);
      div10_step(0x0004);
      div10_step(0x0002);
      div10_step(0x0001);
      #undef div10_step
      if (dividend >= 5) ++quotient; // round the result (optional)
      return quotient;
    }
    

    【讨论】:

    • 您的代码执行 16 乘以 10。为什么您认为您的代码比黑客的喜悦还要快?
    • 我怎么想都无所谓。重要的是在适用的平台上它是否更快。自己试试吧!这里根本没有普遍最快的解决方案。每个解决方案都考虑到某个平台,并且在该平台上运行得最好,可能比任何其他解决方案都好。
    • 我没有注意到 n*10 是常数。因此它将由编译器预先计算。我在答案中提供了另一种算法。我们的算法是等价的,除了一个区别。您从 v 中减去 b*10,然后将其添加到 x*10。您的算法不需要跟踪保存变量的 x*10。您显示的代码展开了我的 while 循环。
    • @chmike:在没有硬件乘法的机器上,n*10 仍然很便宜:(n&lt;&lt;3) + (n&lt;&lt;1)。这些小位移的答案可能对具有缓慢或不存在硬件乘法且仅位移 1 的机器有用。否则,定点逆对于编译时常数除数要好得多(就像现代编译器对 @987654324 所做的那样@)。
    • 这是一个很棒的解决方案,特别适用于没有右移功能的处理器(例如 LC-3)。
    【解决方案6】:

    考虑到 Kuba Ober 的回应,还有一个与此相同的人。 它使用结果的迭代近似,但我不希望有任何令人惊讶的表现。

    假设我们必须找到x,其中x = v / 10

    我们将使用逆运算v = x * 10,因为它有一个很好的属性,当x = a + b,然后x * 10 = a * 10 + b * 10

    让我们使用x 作为变量,保持迄今为止结果的最佳近似值。当搜索结束时,x 将保留结果。我们将设置x 的每一位b 从最重要到最不重要,一个接一个,最后将(x + b) * 10v 进行比较。如果它小于或等于v,则b 位在x 中设置。为了测试下一位,我们只需将 b 向右移动一个位置(除以二)。

    我们可以通过在其他变量中保存x * 10b * 10 来避免乘以10。

    这会产生以下算法,将v 除以 10。

    uin16_t x = 0, x10 = 0, b = 0x1000, b10 = 0xA000;
    while (b != 0) {
        uint16_t t = x10 + b10;
        if (t <= v) {
            x10 = t;
            x |= b;
        }
        b10 >>= 1;
        b >>= 1;
    }
    // x = v / 10
    

    编辑:为了得到 Kuba Ober 的算法,它避免了变量 x10 的需要,我们可以从 vv10 中减去 b10。在这种情况下,x10 不再需要。算法变成了

    uin16_t x = 0, b = 0x1000, b10 = 0xA000;
    while (b != 0) {
        if (b10 <= v) {
            v -= b10;
            x |= b;
        }
        b10 >>= 1;
        b >>= 1;
    }
    // x = v / 10
    

    循环可以展开,bb10 的不同值可以预先计算为常量。

    【讨论】:

    • 呃……这只是二进制而不是十进制的长除法(是的,你在小学学到的东西)。
    • 我不知道你所说的长除法。我敢肯定,我在学校没有学过。我在学校学到的是另一种方法。
    • 我的意思是 en.wikipedia.org/wiki/Long_division#Method ,但是在方法要求您“获得除数的倍数的最大整数”的地方,请记住倍数只能是 1 或 0 时在base-2中工作。您对b10 &lt;= v 的测试只是检查所说的倍数是否为1。无论如何,这就是我几年前为计算机系统架构课程教授长除法的方式。你在学校学了什么十进制长除法?
    • 附带说明,客观上它比十进制长除法更容易,因为您永远不会问自己,例如“3 能除以 8 多少次?”——在 base-2 中,它要么只做一次,没有余数,要么根本不做。唯一让这变得不那么直观的是我们对 base-10 的相对熟悉,而不是在 base-2 中工作。
    【解决方案7】:

    为了扩展 Alois 的答案,我们可以扩展建议的 y = (x * 205) &gt;&gt; 11 以获得更多倍数/班次:

    y = (ms *        1) >>  3 // first error 8
    y = (ms *        2) >>  4 // 8
    y = (ms *        4) >>  5 // 8
    y = (ms *        7) >>  6 // 19
    y = (ms *       13) >>  7 // 69
    y = (ms *       26) >>  8 // 69
    y = (ms *       52) >>  9 // 69
    y = (ms *      103) >> 10 // 179
    y = (ms *      205) >> 11 // 1029
    y = (ms *      410) >> 12 // 1029
    y = (ms *      820) >> 13 // 1029
    y = (ms *     1639) >> 14 // 2739
    y = (ms *     3277) >> 15 // 16389
    y = (ms *     6554) >> 16 // 16389
    y = (ms *    13108) >> 17 // 16389
    y = (ms *    26215) >> 18 // 43699
    y = (ms *    52429) >> 19 // 262149
    y = (ms *   104858) >> 20 // 262149
    y = (ms *   209716) >> 21 // 262149
    y = (ms *   419431) >> 22 // 699059
    y = (ms *   838861) >> 23 // 4194309
    y = (ms *  1677722) >> 24 // 4194309
    y = (ms *  3355444) >> 25 // 4194309
    y = (ms *  6710887) >> 26 // 11184819
    y = (ms * 13421773) >> 27 // 67108869
    

    每一行都是一个单独的、独立的计算,您将在注释中显示的值处看到您的第一个“错误”/不正确结果。对于给定的误差值,您通常最好采用最小的移位,因为这将最大限度地减少在计算中存储中间值所需的额外位,例如(x * 13) &gt;&gt; 7(x * 52) &gt;&gt; 9 “更好”,因为它需要的开销少了两位,而两者都开始给出高于 68 的错误答案。

    如果您想计算更多这些,可以使用以下 (Python) 代码:

    def mul_from_shift(shift):
        mid = 2**shift + 5.
        return int(round(mid / 10.))
    

    当这个近似值开始出错时,我做了很明显的计算:

    def first_err(mul, shift):
        i = 1
        while True:
            y = (i * mul) >> shift
            if y != i // 10:
                return i
            i += 1
    

    (请注意,// 用于“整数”除法,即它向零截断/舍入)

    错误中出现“3/1”模式的原因(即 8 次重复 3 次后跟 9 次)似乎是由于碱基的变化,即 log2(10) 约为 3.32。如果我们绘制错误,我们会得到以下结果:

    相对误差由以下公式给出:mul_from_shift(shift) / (1&lt;&lt;shift) - 0.1

    【讨论】:

    • 你的测试中ms 是什么?
    • @Alexis 我从 Alois 的答案中借用了这个名字,它只是你想要划分的值。也许它是“multiply shift”的缩写?
    • 我明白了,那么每一行的注释值是多少?
    • @Alexis 不确定我是否能比块下的段落更好地解释...这是ms 的第一个值会给出不正确的答案,即参数适用于任何值
    • 很抱歉,我在第一次阅读时没有得到它。谢谢!
    【解决方案8】:

    elemakil 的 cmets 代码可以在这里找到:https://doc.lagout.org/security/Hackers%20Delight.pdf 第 233 页。“无符号除以 10 [和 11。]”

    【讨论】:

    • 仅链接的答案不是 Stack Overflow 的意义所在。如果这涵盖了其他答案中描述的方法,您可以发表评论或提出建议的修改。但这还不足以作为一个单独的答案。或者,您可以引用或总结其中的一些内容并突出显示关键部分,如果即使链接断开也能做出最小的回答。
    【解决方案9】:

    我在 AVR 汇编中设计了一种新方法,仅使用 lsr/ror 和 sub/sbc。它除以 8,然后减去除以 64 和 128 的数字,然后减去第 1,024 和第 2,048,依此类推。工作非常可靠(包括精确舍入)和快速(1 MHz 时为 370 微秒)。 16位数字的源代码在这里: http://www.avr-asm-tutorial.net/avr_en/beginner/DIV10/div10_16rd.asm 获取此源代码的页面在这里: http://www.avr-asm-tutorial.net/avr_en/beginner/DIV10/DIV10.html 我希望它有所帮助,即使这个问题已经存在十年了。 brgs, gsc

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2017-05-24
      • 2016-07-07
      • 1970-01-01
      • 1970-01-01
      • 2018-03-07
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多