我竟在arm汇编除法算法里找到了leetcode某道题的解法

今天讲讲arm汇编中除法的底层实现。汇编代码本身比较长了,如需参考请直接拉到文末。

下面我直接把arm的除法算法的汇编代码转译成C语言的代码贴出来,并进行解析。

因为篇幅有限,所以在此只解析无符号整型的除法运算,关于无符号除法和有符号除法的区别请参考。

代码较长如下,电脑端看效果更佳,如无耐心请直接拉下去看讲解即可:

#include<stdio.h> unsigned int count_leading_zeros(unsigned int num) { unsigned int cnt = 0; while(!(num & 0x80000000) && cnt < 32){ cnt++; num <<= 1; } return cnt; } unsigned int div_unsigned(unsigned int dividend, unsigned int divisor) { unsigned int answer = 0; int cc; unsigned int divisor_lz = 0, dividend_lz = 0; if (divisor == 1){ return dividend; }else if (divisor < 1){ return -1; } if (divisor == dividend){ return 1; }else if (dividend < divisor){ return 0; } if ((divisor & (divisor - 1)) == 0){ return dividend >> (31 - count_leading_zeros(divisor)); } divisor_lz = count_leading_zeros(divisor); dividend_lz = count_leading_zeros(dividend); printf("dividend[0x%x], dividend_lz[%d], divisor[0x%x], divisor_lz[%d]\n", dividend, dividend_lz, divisor, divisor_lz); cc = divisor_lz - dividend_lz; while(cc >= 0){ answer <<= 1; if (dividend >= (divisor << cc)){ answer += 1; dividend -= (divisor << cc); } cc--; } return answer; } main(){ unsigned int a = 0x80000000 / 3; unsigned int b = div_unsigned(0x80000000, 3); printf("[0x%x][0x%x]",a, b); } 2次幂和移位运算

在以上代码中我们终于看到了移位运算对除法运算的优化:
当除数是2的N次幂时,可以直接对被除数做右移运算来代替除法, 比如除数是2即(2的1次幂),此时只需要对被除数做一次右移即可,同理如果除数是8则对被除数做三次右移。

而判断一个数字是不是2的N次幂只需要一行代码:

if ((divisor & (divisor - 1)) == 0){

这一行代码也几乎就是leetcode的第231题2的幂的答案:

2^x n n - 1 n & (n - 1)
2^0   0001   0000   (0001) & (0000) == 0  
2^1   0010   0001   (0010) & (0001) == 0  
2^2   0100   0011   (0100) & (0011) == 0  
2^3   1000   0111   (1000) & (0111) == 0  

如有疑问请继续参考leetcode的题解:https://leetcode-cn.com/problems/power-of-two/solution/power-of-two-er-jin-zhi-ji-jian-by-jyd/

而计算2的N次幂中的N,也只需要这一句即可:

(31 - count_leading_zeros(divisor))

count_leading_zeros即为一个32bit的数字以二进制呈现的时候,从高位向低位数开始数有连续多少个0的数量。

比如数字2的二进制是: 0000 0000 0000 0000 0000 0000 0000 0010
在第一个bit1出现之前有30个0。

判断是否是2的N次幂,并且计算出N的大小并进行右移也只需要以下三行代码。

if ((divisor & (divisor - 1)) == 0){ return dividend >> (31 - count_leading_zeros(divisor)); }

为什么要使用count_leading_zeros这种方法呢,虽然我在上面的代码中定义了函数count_leading_zeros,但是在arm汇编中只需要一条指令clz即可,计算2的N次幂的N加上右移也只需要三条指令即可,非常高效:

clz r2, r1 //计算leading zeros的数量 rsb r2, r2, #31 //31 - count_leading_zeros(divisor) lsr.w r0, r0, r2 // 进行右移 二进制的除法解析

那么更多情况下,除数也并不是2的N次幂。如果除数是3,那么还是要做一下正规的除法了。

我做了一张图来对比8/3的十进制和二进制的除法。

我竟在arm汇编除法算法里找到了leetcode某道题的解法

在二进制时,任何一个bit不可能大于1,所以当两个数字的leading zeros相同时,被除数不可能会整除除数超过或者等于两次。也就是说leading zeros相同时,被除数要么能整除除数一次,要么是0次。

二进制运算除法的时候,首先会对除数做左移操作,让除数和被除数进行“对齐”(即leading zeros数量相同),如果此时的被除数大于等于此时(左移后的)除数,那么在相应的答案位上置一,否则置0。然后对(左移后的)除数​做右移一位操作再继续和被除数做比较,直到除数恢复成原来的初始值(这时候会作最后一次运算)。如下代码所示:

cc = divisor_lz - dividend_lz; while(cc >= 0){ answer <<= 1; if (dividend >= (divisor << cc)){ answer += 1; dividend -= (divisor << cc); } cc--; }

内容版权声明:除非注明,否则皆为本站原创文章。

转载注明出处:https://www.heiqu.com/wpsfgw.html