如何使用位移来代替整数除法?
Posted
技术标签:
【中文标题】如何使用位移来代替整数除法?【英文标题】:How can I use bit shifting to replace integer division? 【发布时间】:2010-10-03 16:55:16 【问题描述】:我知道如何为 2 的幂做这件事,所以这不是我的问题。
例如,如果我想使用位移而不是整数除法来查找数字的 5%,我将如何计算?
所以我可以用 (x * 100 >> 11) 代替 (x * 20 / 19)。现在这是不对的,但它已经接近了,我通过反复试验得出了它。我如何确定要使用的最精确的班次?
【问题讨论】:
为什么?这是为了优化吗?你在优化什么?确定需要优化吗? 是什么让您认为这是可能的? Jonathan 是对的:如果你想把它用作优化,你宁愿让编译器为你做这些工作,因为编译器在做这些事情方面比(大多数)人类做得更好。但是,如果您只是想知道它,我认为没有关于如何在除法和移位之间转换的简短指南。 @Jonathan:+1;不管是什么,这都不是优化... :) @Potatoswatter:不,x * 100 >> 11
是 x * 100 / 2048
是 x * .048828125
,这是 5% 的合理近似值。 x * 102 >> 11
正如brainjam 指出的那样更好,但x * 51 >> 10
一样好并且不太可能溢出,而x * 205 >> 12
的错误要小得多。
【参考方案1】:
最好的办法是让编译器为你做这件事。你只要写
a/b
在您选择的语言中,编译器会生成位旋转。
编辑(我希望你不介意,我正在为你的答案添加强化:
#include <stdio.h>
int main(int argc, char **argv)
printf("%d\n", argc/4);
显然,最快的做法是argc>>2
。让我们看看会发生什么:
.file "so3.c"
.section .rodata
.LC0:
.string "%d\n"
.text
.globl main
.type main, @function
main:
pushl %ebp
movl %esp, %ebp
andl $-16, %esp
subl $16, %esp
movl 8(%ebp), %eax
movl %eax, %edx
sarl $31, %edx
shrl $30, %edx
leal (%edx,%eax), %eax
sarl $2, %eax
movl %eax, %edx
movl $.LC0, %eax
movl %edx, 4(%esp)
movl %eax, (%esp)
call printf
leave
ret
.size main, .-main
.ident "GCC: (Ubuntu 4.4.3-4ubuntu5) 4.4.3"
.section .note.GNU-stack,"",@progbits
是的,就是这样,sarl $2, %eax
EDIT 2 (抱歉,继续往下看,但20/19
有点复杂……)
我刚刚将argc*20/19
替换为argc/4
,得出的数学公式如下:
0000000100000f07 shll $0x02,%edi
0000000100000f0a movl $0x6bca1af3,%edx
0000000100000f0f movl %edi,%eax
0000000100000f11 imull %edx
0000000100000f13 sarl $0x03,%edx
0000000100000f16 sarl $0x1f,%edi
0000000100000f19 subl %edi,%edx
所以,过程是
将输入乘以 4 (shll) 加载 (movl 0x...) 并乘以 (imull) 获得 64 位结果的定点小数(这是 32 位代码) 将结果的高 32 位除以 8 (sarl),注意它如何处理负数 将结果的低 32 位除以 INT_MAX (sarl) 以获得 0 或 -1 如有必要,通过加 1(减去 -1)来正确舍入高阶结果。【讨论】:
+1 - 手工计算比特是一件苦差事,学习这个过程的最好方法是查看编译输出。 我添加了编译器输出来证明你是多么正确! @Potatoswatter:我跌倒了 baaaddd,从你的努力中获得了如此多的代表。不是很糟糕,它不会让我在晚上保持清醒,但有点糟糕:-) @Mark:嗯,如果我不费吹灰之力地笼统地描述它,那会更有帮助。让代表实际决定任何事情毫无意义。【参考方案2】:这没有任何意义,因为您尝试做的事情并没有优化结果过程!!!
嘿,我没有在您的问题中看到您打算优化的任何地方。
Electrical Engg 人永远不会停止好奇,无论“有用”如何。我们就像那些你在新闻中读到的东西的强迫症囤积者,他们把阁楼、地窖、卧室和客厅里堆满了他们认为有一天会派上用场的垃圾。至少在不到 30 年前,我在英格学校时就是这种情况。我鼓励你继续寻求囤积“无用”的知识,这些知识似乎不太可能优化你的生活或生活方式。当您可以通过手动编码算法完成时,为什么还要依赖编译器?!啊?有点冒险精神,你知道的。 好吧,去鄙视那些对你追求知识表示不屑的人。
还记得在你的中学,你被教导做除法的方式吗? 437/24,例如
_____
24|437
018
-----
24|437
24
-----
197
24
-----
5
被除法的数字 437 称为被除数。 24 是除数,结果 18 是商,5 是余数。 就像您报税时一样,您需要填写从股票“股息”中获得的利润,这是用词不当。您填写的税表是一大笔股息的商数的倍数。您没有收到股息,而是部分股息 - 否则,这意味着您拥有 100% 的股票。
___________
11000|110110101
000010010
-----------
11000|110110101
11000
----------
000110101 remainder=subtract divisor from dividend
11000000 shift divisor right and append 0 to quotient until
1100000 divisor is not greater than remainder.
110000 Yihaa!
----------
000101 remainder=subtract shifted divisor from remainder
11000 shift divisor right and append 0 to quotient until
1100 divisor is not greater than remainder.
----------
oops, cannot shift anymore.
您可能已经知道,以上是真正的除法。这是通过减去一个移位的除数来实现的。
您想要的是通过简单地转移红利来实现相同的目标。不幸的是,除非除数是 2 (2,4,8,16) 的指数幂,否则无法做到这一点。这是二进制算术的一个明显事实。或者,至少我不知道有任何方法可以在没有近似和内插技术的情况下做到这一点。
因此,您必须结合使用除法移位和真除法。 例如
24 = 2 x 2 x 2 x 3
首先,用二进制移位将437除以8得到010010,然后用真除法除以3:
010010
--------
11|110110
11
-------
011
11
-----
0
计算结果为 010010 = 18。
瞧。
你如何确定 24 = 2^8 x 3?
向右移动 11000 直到达到 1。
这意味着,您可以移动除数的次数与移动除数相同的次数,直到除数达到 1。
因此,很明显,如果除数是奇数,此方法将不起作用。 例如,它不适用于除数 25,但它会适用于除数 50。
可能是,有一些预测方法可以将 13 这样的除数插值在 2^3=8 和 2^4=16 之间。如果有,我不熟悉。
您需要探索的是使用数字系列。例如除以 25:
1 1 1 1 1
__ = __ - ___ - ___ + ___ - ... until the precision you require.
25 16 64 128 256
系列的一般形式在哪里
1 1 b1 bn
_ = ___ + _______ + ... + ______
D 2^k 2^(k+1) 2^(k+n)
其中 bn 为 -1、0 或 +1。
我希望我上面的二进制操作不会有错误或拼写错误。如果是这样,成千上万的人道歉。
【讨论】:
【参考方案3】:假设您有表达式a = b / c
。正如 hroptatyr 所提到的,乘法非常快(而且比除法快得多)。所以基本思想是将除法转换为乘法,例如:a = b * (1/c)
。
现在,我们仍然需要除法来计算倒数 1/c
,所以这只有在 c
是先验已知的情况下才有效。虽然对于浮点计算就足够了,但对于整数,我们必须使用另一个技巧:我们可以使用 some_big_number / c
的值作为 c
的倒数,这样最后我们将计算 a2 = b * (some_big_number / c)
,即等于some_big_number * b/c
。因为我们对b/c
的值感兴趣,所以我们必须将最终结果除以some_big_number
。如果选择 2 的幂,那么最后的除法会很快。
例如:
// we'll compute 1/20 of the input
unsigned divide_by_20(unsigned n)
unsigned reciprocal = (0x10000 + 20 - 1) / 20; //computed at compile time, but you can precompute it manually, just to be sure
return (n * reciprocal) >> 16;
编辑:此方法的一个很好的部分是您可以通过选择更正来为除法选择任何舍入方法(在这种情况下,它是 20 - 1
用于舍入为零)。
【讨论】:
对于有符号值,除以 65536 而不是移位 16,编译器将转换为移位和修正。【参考方案4】:如果您对其背后的数学感兴趣,请阅读 Henry S. Warren 的Hacker's Delight。
如果您对优化代码感兴趣,只需编写人类最容易阅读的内容即可。例如:
int five_percent(int x)
return x / 20;
当您使用g++ -O2
编译此函数时,它不会进行实际除法,而是进行一些魔术乘法、位移和校正。
【讨论】:
【参考方案5】:你不能用轮班来做所有事情,你需要使用“魔法”除数(见黑客的喜悦)。魔术除法的工作原理是将一个数字乘以另一个适当大的数字,然后将其翻转以产生除法的答案(mul/imul 比 div/idiv 快)。魔术常数仅对每个素数唯一,倍数需要移位,例如:无符号除以 3 可以表示(在 32 位上)为x * 0xAAAAAAAB
,除以 6 将是 (x * 0xAAAAAAAB) >> 1
除以 12 将移位 2, 24 x 3 等(其几何级数 3 * (2 ^ x)
,其中 0
【讨论】:
【参考方案6】:假设您想通过乘以 y 并移动 n 来近似 x 的 5%。由于 5% 是 1/20,并且 a>>n = a/2n,所以你要解决
x/20 ≈ x*y/2n(符号“≈”表示“近似相等”)
简化为
y ≈ 2n/20
所以如果 n=11,那么
y ≈ 2n/20 = 2048/20 =102 + 8/20
所以我们可以设置 y=102,这实际上比你通过反复试验找到的 100 更好。
一般我们可以和n一起玩,看看能不能得到更好的答案。
我已经计算出了分数 1/20,但是您应该能够按照相同的方法计算出任何分数 p/q。
【讨论】:
【参考方案7】:一般来说:
获得数字的素数分解,将 N 分解为 2^k * 余数,然后可以对两个幂使用位移。示例:20 = 2^2 * 5,所以要乘以 20,您需要乘以 5,然后使用位移<< 2
要对非二次幂使用位移,请注意以下奇数l
:a * l = a * (l - 1) + a
,现在l - 1
是偶数,因此分解为二次幂,位移“技巧”适用于此。
可以类似地构造除法。
【讨论】:
这毫无意义。乘以 5 包括移动<< 2
的任何成本。这里的目的是在没有除法的一两条指令中乘以任何有理数,而不是分解数字并使用不定数量的 insn。
谁说的? OP想知道如何将整数乘法转换为位移,我刚刚描述了一般过程。
哦,顺便说一句,在您测量之前永远不要判断,我刚刚发现 imul
在我的 CPU 上将是 3 个周期,而我的解决方案使用 shl
和 add
需要 2 个周期。
一个shl
和一个add
只完成乘以5。你仍然需要另一个insn 来再次移位。编译器应该足够聪明,可以弄清楚它,如果它真的很差,就不会产生imul
,尽管出于可移植性考虑,它可能不是专门针对您的芯片的,并且更高的指令数可能会导致其他拥塞。
无论如何,问题不在于将乘法替换为除法,而您根本没有解决这个问题。这需要获得乘法的高阶结果,而不能使用 C 运算符表示。 (至少,没有获得整数寄存器的全宽。)这是一个定点数学技巧。以上是关于如何使用位移来代替整数除法?的主要内容,如果未能解决你的问题,请参考以下文章