参考 Binary to Decimal Conversion in Limited Precision
版权所有 © 1999,Douglas W. Jones。2002 年进行了重大修订,2007 年进行了少量更新。只要包含本声明,本作品就可以以电子形式传输或存储在任何连接到互联网或万维网的计算机上。个人可以复制一份供自己使用。保留所有其他权利。
介绍
程序员在计算机上处理文本输入输出时一直面临计算问题,因为我们习惯将打印数字用十进制表示。
这些问题从来都不是无法克服的,但是在为运算能力有限的机器编写程序时,它们会特别令人烦恼。
假设, 例如,你有一个想要以十进制形式打印的无符号二进制数。 以下 C 代码将完成此操作:
void putdec(uint16_t n)
{
uint16_t rem = n % 10;
uint16_t quot = n / 10;
if (quot > 0) putdec(quot);
putchar(rem + '0');
}
问题在于 C 代码假设计算机的运算单元足够大,可以处理整个数字 n,并且可以方便地进行整数除法,同时得到商和余数。
这些假设并不总是合理的,因为计算机不一定有除法硬件,即使有除法硬件,对于超出 ALU 处理能力的数字来说也作用不大。
本教程的重点是编写快速代码,在没有除法指令的 8 位 ALU 机器上将无符号 16 位二进制数转换为十进制数。这种情况在有些单片机上会出现,并且这些技术可以推广用于其他场景,例如使用 32 位 ALU 将 64 位二进制数转换为十进制数。
对于没有除法指令的机器,如果速度不是问题,可以使用反复减法代替本教程中给出的快速但复杂的代码。例如,以下转换代码。
void putdec(int16_t n)
{
uint8_t a, b, c, d;
if (n < 0) {
putchar('-');
n = -n;
}
for (a = '0' - 1; n >= 0; n -= 10000) ++a;
for (b = '9' + 1; n < 0; n += 1000) --b;
for (c = '0' - 1; n >= 0; n -= 100) ++c;
for (d = '9' + 1; n < 0; n += 10) --d;
putchar(a);
putchar(b);
putchar(c);
putchar(d);
putchar(n + '0');
}
这种算法的变种在 8 位微处理器非常流行,尽管这个代码的转换过程中,for 循环可能需要执行 30 次以上。
基本思想
为了更快地进行转换,把二进制数视为一个 16进制数,然后根据其 16进制位进行转换。考虑一个 16 位的数字 n。它可以分解成 4 个 4 位的字段,即 4 个 16 进制位,记为 n3、n2、n1 和 n0。
这四个段,我们可以用以下方式表示数字的值:
n = 4096n3 + 256n2 + 16n1 + n0
就像之前将 16 位二进制数拆分成 4 个 16 进制位一样,如果用十进制表示 n 的值,可以写成 d4 d3 d2 d1 d0,其中每个 d4、d3、d2、d1、d0 都是十进制数字 (0 到 9)。16 位二进制能表示的最大十进制数65535(五位数)。
n = 10000d4 + 1000d3 + 100d2 + 10d1 + d0
现在的问题是如何计算各个十进制位 (d4 d3 d2 d1 d0) 的值,而不用直接处理整个更大的 16 位二进制数 n。
为了实现这一点,首先注意 (n3 n2 n1 n0)的系数本身可以表示成 10的x次方倍数之和(x属于自然数)。
n =
n3 ( 4×1000 + 0×100 + 9×10 + 6×1 ) +
n2 ( 2×100 + 5×10 + 6×1 ) +
n1 ( 1×10 + 6×1 ) +
n0 ( 1×1 )
如果我们将 (n3 n2 n1 n0) 分配到的括号内表达式中,然后合并10的x次方,得到以下结果:
n =
1000(4n3 ) +
100(0n3 + 2n2 ) +
10(9n3 + 5n2 + 1n1 ) +
1(6n3 + 6n2 + 6n1 + 1n0 )
基于这个分解,可以得到 (d4 d3 d2 d1 d0)的估计值(a3 a2 a1 a0)。
a3 = 4n3
a2 = 0n3 + 2n2
a1 = 9n3 + 5n2 + 1n1
a0 = 6n3 + 6n2 + 6n1 + 1n0
由于没有将(a3 a2 a1 a0) 的值限制在 0 到 9 的范围内,不是严格的十进制数字. 考虑到这一点,以及每个(n3 n2 n1 n0) 的取值范围为 0 到 15 (0 ≤ ni < 15),我们可以得到 ai 的取值范围如下:
0 < a3 < 60
0 < a2 < 30
0 < a1 < 225
0 < a0 < 285
虽然计算出的初始值 a1 到 a3 超出正常的十进制范围 (0 到 9)。但其实是个好消息,因为这些值都小于 256,因此可以使用 8 位的算术逻辑单元 (ALU) 进行计算。如果可以利用 ALU 的进位输出位来存储最高位,那么也可以使用 8 位进行计算。此外,如果关注的是二进制补码运算 (最高位是符号位),那么输出过程的第一步是打印负号然后取反。因此,对 n3 的限制变为 0 <= n3 < 8,我们可以推断出
0 < a0 < 243
注意之前提到的是 n3 < 8 而不是 n3 < 7。这是因为在二进制补码系统中存在一个负数无法正确取反,即 -32768。如果将这个数字排除在合法值的范围之外,那么就会降低到 255以下;n ( -32768 ),n3 为 8 ,(n2 n1 n0) 都是 0,因此:
0 < a0 < 237
可以看出可以安全地使用 8 位运算来累加所有的 ai。既然我们已经计算出了 ai 的值,可以通过一系列 8 位除法和一个 9 位除法将它们调整到正确的值域:
carry表示进位,缩写c1 c2 c3 c4
c1 = a0 / 10
d0 = a0 % 10
c2 = (a1 + c1 ) / 10
d1 = (a1 + c1 ) %10
c3 = (a2 + c2 ) / 10
d2 = (a2 + c2 ) % 10
c4 = (a3 + c3 ) / 10
d3 = (a3 + c3 ) % 10
d4 = c4
上述公式中,(c1 c2 c3 c4)项代表进位传递到下一位。每个 ci 的取值可以由 ai 计算得出:
0 < a3 < 60
0 < a2 < 30
0 < a1 < 225
0 < a0 < 285
c1 < 28 [ = 285/10 ]
c2 < 25 [ = (28 + 225)/10 = 253/10 ]
c3 < 5 [ = (25 + 30)/10 = 55/10 ]
c4 < 6 [ = (5 + 60)/10 = 65/10 ]
当计算 c2 的上限时,得出的值非常接近 255 (8 位 ALU 可表示的最大值),仅差 2。这表明使用 8 位 ALU 来进行计算几乎是够用的。
再次强调,如果我们在输出之前对所有负数取反,使 n3 的上限变为 8,那么 c1 的上限将变为 23 而不是 28 (0 < a0 < 237)。
对上述进行编码,涉及所有使用过的临时变量,如果稍微重组一下代码,并注意到在计算每个数字之后,我们不再需要其中一个 ni,那么可以避免这样做。 以下是用 C 代码实现的示例:
void putdec(uint16_t n)
{
uint8_t d4, d3, d2, d1, q;
uint16_t d0;
d0 = n & 0xF;
d1 = (n >> 4) & 0xF;
d2 = (n >> 8) & 0xF;
d3 = (n >> 12) & 0xF;
d0 = 6 * (d3 + d2 + d1) + d0;
q = d0 / 10;
d0 = d0 % 10;
d1 = q + 9 * d3 + 5 * d2 + d1;
q = d1 / 10;
d1 = d1 % 10;
d2 = q + 2 * d2;
q = d2 / 10;
d2 = d2 % 10;
d3 = q + 4 * d3;
q = d3 / 10;
d3 = d3 % 10;
d4 = q;
putchar(d4 + '0');
putchar(d3 + '0');
putchar(d2 + '0');
putchar(d1 + '0');
putchar(d0 + '0');
}
上面的代码会打印 n 的所有 5 个数字,而不是只打印有效数字,但这可以通过多种方式来解决。以下的解决方法可能是最简洁的。
void putdec(uint16_t n)
{
uint8_t d4, d3, d2, d1, q;
uint16_t d0;
d0 = n & 0xF;
d1 = (n >> 4) & 0xF;
d2 = (n >> 8) & 0xF;
d3 = (n >> 12);
d0 = 6 * (d3 + d2 + d1) + d0;
q = d0 / 10;
d0 = d0 % 10;
//如果d1值为0,d3 d2都为0,不用打印输出
d1 = q + 9 * d3 + 5 * d2 + d1;
if (d1 != 0) {
q = d1 / 10;
d1 = d1 % 10;
d2 = q + 2 * d2;
if ((d2 != 0) || (d3 != 0)) {
q = d2 / 10;
d2 = d2 % 10;
d3 = q + 4 * d3;
if (d3 != 0) {
q = d3 / 10;
d3 = d3 % 10;
d4 = q;
if (d4 != 0) {
putchar(d4 + '0');
}
putchar(d3 + '0');
}
putchar(d2 + '0');
}
putchar(d1 + '0');
}
putchar(d0 + '0');
}
处理有符号数
为了打印有符号值,可以用以下代码替换上面代码的第一行:
void putdec(int16_t n)
{
uint8_t d4, d3, d2, d1, d0, q;
if (n < 0) {
putchar('-');
n = -n;
}
d0 = n & 0xF;
d1 = (n >> 4) & 0xF;
d2 = (n >> 8) & 0xF;
d3 = (n >> 12);
//.............
//省略后面的代码
//.............
}
在n 分解之前转为正数,我们可以使用 uint8_t类型来表示 d0 而不是 uint16_t 类型。
需要注意的是,由于从无符号运算改为有符号运算,d3 的计算也发生了变化。这是因为右移运算符在应用于无符号数时会将 0 填充到最高位,而在应用于有符号数时会复制符号位。在 16 位二进制补码运算中,存在一种特殊情况,即 n 和 -n 都为负数,也就是 n = -32768。进行更改可以在这种情况下提供正确的输出。
限制在 8 位内
在上面用于无符号打印的代码中,由于 a0 可能高达 285,我们不得不将变量 d0 声明为 uint16_t 而不是 uint8_t。可以判断(6 * (d3 + d2 + d1) + d0)是否超过 255 来避免这一点。检测到溢出后,可以在接下来的计算中考虑溢出的情况:
void putdec(uint16_t n)
{
uint8_t d4, d3, d2, d1, d0, q;
d0 = n & 0xF;
d1 = (n >> 4) & 0xF;
d2 = (n >> 8) & 0xF;
d3 = (n >> 12) & 0xF;
if ((6 * (d3 + d2 + d1) + d0) > 255) { /* 溢出 */
d0 = (6 * (d3 + d2 + d1) + d0) & 0xFF;
q = d0 / 10 + 25;
d0 = d0 % 10 + 6;
if (d0 >= 10) {
d0 = d0 - 10;
q = q + 1;
}
}
else { /* 没有溢出 */
d0 = 6 * (d3 + d2 + d1) + d0;
q = d0 / 10;
d0 = d0 % 10;
}
//.............
//省略后面的代码
//.............
}
在上面用于处理无符号数的代码中,当我们注意到计算 a0 时发生了溢出后,我们执行了以下步骤:
1、保留最低有效位: 将溢出后计算结果的最低有效位保存到 d0 中。
2、近似修正商: 由于溢出增加了额外的值,需要近似地修正商。这里添加了 256/10 (约等于 25) 来补偿溢出的影响。
3、修正余数: 修正商之后还需要计算精确的余数,这部分的计算稍微复杂一点。
下面解释了修正余数的思路:
(i + j ) % m = ((i % m ) + (j % m )) % m
给定一个大于 256 的整数 n,如果想得到 n %10 的值,需要注意模运算满足分配律 。
n %10 = (((n - 256) % 10) + (256 %10)) % 10
= (((n - 256) %10) + 6) %10
如果((n - 256) %10) + 6 大于 10,还需要额外加上 1 来修正商的误差。
无乘除法指令
商 = n/10 = n*0.1 (0.1 转为二进制小数0.00011001101)
商 = (n>>4)+(n>>5)+(n>>8)+(n>>9)+(n>>11)
商 = (n+(n>>1)+(n>>4)+(n>>5)+(n>>7))>>4
商 = (n+(n>>1)+((n+(n>>1)+(n>>3) )>>4))>>4
商 = (n+(n>>1)+(n+((n+(n>>2))>>1))>>4))>>4
q1 = (n+(n>>2))>>1
商 = (n+(n>>1)+((n+(q1))>>4))>>4
q2 = (n+(q1))>>1
商 = (n+(n>>1)+(q2>>3))>>4
商 = (n+((n+(q2>>2))>>1))>>4
q3 = (n+(q2>>2))>>1
q4 = (n+(q3))>>4
商 = q4
uint8_t q /*商*/, r /*余数*/;
//求n除10的商和余数函数
void div10(uint8_t n)
{
/* 商 q = n*(1/10) (1/10转为二进制小数0.00011001101) */
q = ((n >> 2) + n) >> 1;
q = ((q)+n) >> 1;
q = ((q >> 2) + n) >> 1;
q = ((q)+n) >> 4;
/* 余数 r = n - 10*q */
r = ((q << 2) + q) << 1; /* 10转为二进制 1010 */
//r = q*(2^3+2^1)
//r = (q<<3)+(q<<1)
//r = ((q<<2)+q)<<1
r = n - r;
}
商 = n/10 = n*0.1
(0.1 转为二进制小数0.00011001101)
[二进制小数(0.00011001101) = 二进制整数(11001101>>11)]
[二进制整数(11001101)=十六进制(0xCD)]
商 = (n*0xCD)>>11
[把0.1精度缩小的二进制小数(0.00011010)=(11010>>8)=(0x1A >> 8)]
void putdec(int16_t n)
{
uint8_t d4, d3, d2, d1, d0, q;
if (n < 0) {
putchar('-');
n = -n;
}
d1 = (n >> 4) & 0xF;
d2 = (n >> 8) & 0xF;
d3 = (n >> 12) & 0xF;
d0 = 6 * (d3 + d2 + d1) + (n & 0xF);
q = (d0 * 0xCD) >> 11;
d0 = d0 - 10 * q;
d1 = q + 9 * d3 + 5 * d2 + d1;
q = (d1 * 0xCD) >> 11;
d1 = d1 - 10 * q;
d2 = q + 2 * d2;
q = (d2 * 0x1A) >> 8;
d2 = d2 - 10 * q;
d3 = q + 4 * d3;
d4 = (d3 * 0x1A) >> 8;
d3 = d3 - 10 * d4;
putchar(d4 + '0');
putchar(d3 + '0');
putchar(d2 + '0');
putchar(d1 + '0');
putchar(d0 + '0');
}
static noinline char *put_dec(char *buf, uint64_t num)
{
while (1) {
unsigned rem;
if (num < 100000)
return put_dec_trunc(buf, num);
rem = do_div(num, 100000);
buf = put_dec_full(buf, rem);
}
}
static char *put_dec_trunc(char *buf, unsigned q)
{
unsigned d3, d2, d1, d0;
d1 = (q>>4) & 0xf;
d2 = (q>>8) & 0xf;
d3 = (q>>12);
d0 = 6*(d3 + d2 + d1) + (q & 0xf);
q = (d0 * 0xcd) >> 11;
d0 = d0 - 10*q;
*buf++ = d0 + '0'; /* least significant digit */
d1 = q + 9*d3 + 5*d2 + d1;
if (d1 != 0) {
q = (d1 * 0xcd) >> 11;
d1 = d1 - 10*q;
*buf++ = d1 + '0'; /* next digit */
d2 = q + 2*d2;
if ((d2 != 0) || (d3 != 0)) {
q = (d2 * 0xd) >> 7;
d2 = d2 - 10*q;
*buf++ = d2 + '0'; /* next digit */
d3 = q + 4*d3;
if (d3 != 0) {
q = (d3 * 0xcd) >> 11;
d3 = d3 - 10*q;
*buf++ = d3 + '0'; /* next digit */
if (q != 0)
*buf++ = q + '0'; /* most sign. digit */
}
}
}
return buf;
}
static char *put_dec_full(char *buf, unsigned q)
{
/* BTW, if q is in [0,9999], 8-bit ints will be enough, */
/* but anyway, gcc produces better code with full-sized ints */
unsigned d3, d2, d1, d0;
d1 = (q>>4) & 0xf;
d2 = (q>>8) & 0xf;
d3 = (q>>12);
/*
* Possible ways to approx. divide by 10
* gcc -O2 replaces multiply with shifts and adds
* (x * 0xcd) >> 11: 11001101 - shorter code than * 0x67 (on i386)
* (x * 0x67) >> 10: 1100111
* (x * 0x34) >> 9: 110100 - same
* (x * 0x1a) >> 8: 11010 - same
* (x * 0x0d) >> 7: 1101 - same, shortest code (on i386)
*/
d0 = 6*(d3 + d2 + d1) + (q & 0xf);
q = (d0 * 0xcd) >> 11;
d0 = d0 - 10*q;
*buf++ = d0 + '0';
d1 = q + 9*d3 + 5*d2 + d1;
q = (d1 * 0xcd) >> 11;
d1 = d1 - 10*q;
*buf++ = d1 + '0';
d2 = q + 2*d2;
q = (d2 * 0xd) >> 7;
d2 = d2 - 10*q;
*buf++ = d2 + '0';
d3 = q + 4*d3;
q = (d3 * 0xcd) >> 11; /* - shorter code */
/* q = (d3 * 0x67) >> 10; - would also work */
d3 = d3 - 10*q;
*buf++ = d3 + '0';
*buf++ = q + '0';
return buf;
}
# define do_div(n,base) ({ \
uint32_t __base = (base); \
uint32_t __rem; \
(void)(((typeof((n)) *)0) == ((uint64_t *)0)); \
if (((n) >> 32) == 0) { \
__rem = (uint32_t)(n) % __base; \
(n) = (uint32_t)(n) / __base; \
} else \
__rem = __div64_32(&(n), __base); \
__rem; \
})
uint32_t __div64_32(uint64_t *n, uint32_t base)
{
uint64_t rem = *n;
uint64_t b = base;
uint64_t res, d = 1;
uint32_t high = rem >> 32;
/* Reduce the thing a bit first */
res = 0;
if (high >= base) {
high /= base;
res = (uint64_t) high << 32;
rem -= (uint64_t) (high*base) << 32;
}
while ((int64_t)b > 0 && b < rem) {
b = b+b;
d = d+d;
}
do {
if (rem >= b) {
rem -= b;
res += d;
}
b >>= 1;
d >>= 1;
} while (d);
*n = res;
return rem;
}