使用移位和加/减除以常数

问题描述 投票:0回答:4

大家好,我正在尝试仅使用移位和加/减除以无符号常量 - 如果它是乘法,我对此没有问题,但我对除法有点困惑。

例如,假设常除数是 192,被除数是 8000

“完整结果”y = 8000/192 = 41(假设我不保留小数位)

y = 8000 >> 8 ... 31 y = 8000 >> 7 ... 62

但是如何获得更准确的解决方案?

非常感谢!

performance assembly bit-manipulation math approximation
4个回答
10
投票

几乎肯定有一种更优雅的方法来做到这一点,但这足以让您开始。

这种除法通常是通过乘以倒数来完成的,即先乘后右移。

(注意:请记住,乘法可以通过移位和加法来完成(例如

n * 3 = (n*2) + (n*1) = (n << 1) + (n) )
,但我只想在这里使用乘法。你的问题说“移位和加法”,我正在证明我对乘法的速记使用是合理的)

在下面的示例中,我尝试用示例来解释这些概念。根据您的具体情况,您需要考虑以下问题:

  1. 符号(我在下面使用无符号整数)

  2. 溢出(下面我使用 32 位无符号长整型来保存中间值,但如果您使用的是较小的 uC,请注意,请进行相应调整

  3. 四舍五入(例如 9/5 应该返回 1 还是 2?在 C 语言中,它是 1,但也许您想要 2,因为它更接近正确答案?)

  4. 在可以的范围内(可用位),在除法之前进行所有乘法(最小化截断错误)。再次强调,要注意溢出。

正如我所说,请阅读以下内容以了解概念,然后根据您的需求进行定制。

除以 192 与乘以 1/192 相同,与除以 (64 * 3) 相同。 1/3 没有精确(有限)的二进制表示,因此我们用 0x5555/(1 << 16).

要除以 192,我们先除以 64,然后除以 3。要除以 3,我们乘以 0x5555 并右移 16(或乘以 0x55 并 >> 8,或...)

//                8000/192          =
//                ((8000/64)/3)     =
//                ((8000 >> 6) / 3) =
//                (((8000 >> 6) * 0x5555) >> 16)
//                (((8000 * 0x5555) >> 22

请注意,括号是故意的。你不想想要计算

(8000 * (0x5555/(1 << 16))
,因为第二项是0,而乘积将是0。不好。

因此,代码中的 1 行代码将类似于:

 printf("Answer:  %lu\n", ((8000UL * 0x5555UL) >> 22));

这将产生 41,这就是“C”对

8000/192
产生的结果,尽管 42“更接近”。通过检查 LSB,您可以根据需要进行舍入。

有人可以就这个主题写一篇论文,但幸运的是比我聪明得多的人已经写了


5
投票

我开发了一个常数除法生成器,可以轻松地为您提供任何常数的优化除法。它遵循“黑客的喜悦”的想法。

名为“kdiv”的工具可在 sourceforge 上找到:

http://sourceforge.net/projects/kdiv/


2
投票

我会看黑客乐趣书来了解这类事情。我没有随身携带副本,但与此无关,如果您查看除数 192,即 0xC0,因此您可以将顶部和底部除以 0x40,移位 8000>>6 = 125。8000/192 -> 125/ 3,但是你必须除以 3。我们知道答案将在 125/2 和 125/4 之间。对于这些特定数字,125 是 0x7d 或 b1111101,它是 b100000 + 11101 的 3 倍,即 (3 乘以 0x20) + (3 乘以 8) + 5,因此 125/3 = 0x20 + 0x8 + (5/3) 并且 5/3 是很快就确定为大于 1 但小于 2,因此 0x28+1 = 41。只有当除数位模式持续出现在分子位模式的高位时,才会继续随移位而减小。我不知道黑客高兴或其他类似的消息来源对此事有何评论,我只是碰巧注意到这些特定数字的这种模式。


0
投票

如果将一组降档相加,则除以 2 的非幂。

如何获得所需的幂是获取最高有效位并将其除以数字,然后这将输出数字以及将它们加在一起以获得所需除法的位。

问题是,它需要除法才能得到数字,所以你只能除以常数。

有一种无需除法即可计算数字的方法,但在硬件中,从表面上看,它与带有条件的除法看起来没有太大不同或更好,但我不确定它是更好还是更差。

但是如果你想进行变量除法,请获取最高有效位并将其乘以除法的数字。 (所以一开始就需要乘法,但你有这些变量。)

然后将其向下移动 2。

然后检查其是否高于或低于 MSB,即 MSB 作为除法和中的组成部分是否打开或关闭。

再次向下移动 2。

您的值要么高于上次测试,要么低于上次测试这个数字,具体取决于您作为最后一个显着性的组成部分是开启还是关闭。

继续这样下去,就可以进行可变班次除法了。

所以它->


    CIELING=MSB*DIVIDING NUMBER // do this via sum of shifts.
CHECK1=CIELING/2
if(above CHECK1) { component0=on. }
            else {component0=off.}
CHECK2=CIELING/4
if(component0 on) { CHECK2=CHECK1+CHECK2}
if(above CHECK2) { component1=on. }
            else {component1=off.}
CHECK3=CIELING/8
if(component1 on) { CHECK3=CHECK2+CHECK3}
if(above CHECK3) { component2=on. }
            else {component2=off.}

//then once have all components, do the final sum of shifts.

CHECK1=CIELING/2
if(above CHECK1) { component0=on. }
            else {component0=off.}
CHECK2=CIELING/4
if(component0 on) { CHECK2=CHECK1+CHECK2}
if(above CHECK2) { component1=on. }
            else {component1=off.}
CHECK3=CIELING/8
if(component1 on) { CHECK3=CHECK2+CHECK3}
if(above CHECK3) { component2=on. }
            else {component2=off.}

© www.soinside.com 2019 - 2024. All rights reserved.