大数表示
math/big包中定义了数值超出64位的大数数据结构和有关函数。
type Int struct {//表示一个大数
neg bool // 符号,为true时表示负数
abs nat // 绝对值
}
type nat []Word
type Word uint
大数的绝对值存储在abs中,实际上就是将绝对值的二进制表示从右至左按64位切块(这里均以64位字长为例),依次放入abs[0]、abs[1]、… 。
用公式描述就是将大数以264为基数展开,形如xn-1264(n-1)+xn-2264(n-2)+ … + x1264+x0,按由右至左的顺序记录各个系数xi ,即abs[0]=x0,abs[1]=x1,… 。这里0 <= xi < 264,n为切片长度,显然xn-1不等于0。
虽然看起来比较复杂,但实质上与我们熟悉的十进制表示法完全相同,只不过现在是264进制,也就是以264为基数,系数不是10以内,而是264以内。例如:十进制的8705表示的是8x103+7x102+0x10+5这个数,它的系数是8、7、0、5;如果换成是264进制,它表示的就是8x264x3+7x264x2+0x264+5这个数。也可以说abs存储的是264进制表示法的系数。
感觉上,大数表示法似乎应该减少了存储空间的占用,实际上是错觉,不考虑符号存储问题,它和原生态地存储大数占用的空间是一样的,对于任意大数的每一个比特都要精确表达。
初始化大数
func NewInt(x int64) *Int
以x为基础新建一个大数,并返回Int型指针。当机器字长为32位时,该函数会将x拆成2个系数。更有用的方法是:
func (z *Int) SetString(s string, base int) (*Int, bool)
该方法将字符串s解释为以base为基数的一个大数,并填充z,返回的bool值表示成功与否。base为0时,由s的前缀决定字符串所代表的数字进制,0-八进制,0x或0X-十六进制,0b或0B-二进制,其它-十进制。比如:
var a big.Int
a.SetString(“0x742e666d7400747970652e2e65712e662e74016d”,0)
定义了一个20字节长的大数。这里不接收SetString()的返回值也没有问题。这个方法的实现有点意思,为了效率使用了汇编语言,下面作一简要剖析。
- 将字符串s转化为一个字节流。
- 确定符号,然后把后续的字节流都当成无符号数来处理,即绝对值。
- 确定进制,如是否为0、0x、0b先导等等。
- 根据进制,确定一个字长能够容纳的该进制数字的个数n和基本权重bn,比如:64位字长可容纳的最大十进制数为1.8447x1019,共20个数字,这里取n=19,基本权重为1019。因为如果取20可能会“冒顶”,如2x1019也是20个数字但放不进一个字长内。所以这个n实际上是该进制能安全地存入一个字长的最大数字个数。至于基本权重将用于后续进制转换计算。从这里还能预感到一个问题,比如1x1019虽然是20个数字,但能放在一个字长内,golang是不是用了两个字长?
- 从现在开始循环读入字节流,每n个数字调用方法mulAddWW将值写入系数切片。首次写入就是简单的赋值,之后的写入都要调用mulAddVWW这个汇编函数来处理。为了看清这个函数的过程,先以十进制为例在纸面上推导一下添加切片元素的过程。
- 设初始[]abs={a0}
- 添加r0后,大数N0 = a0 x 1019 + r0,机器内部表示将是一个{高64位,低64位}对,比如N0 = b1 x 264 + b0 ,则[]abs={b0, b1}
- 再添加r1后,N1 =b1 x 264 x 1019 + b0 x 1019 + r1,机器内部表示将是一个{高64位,中64位,低64位}对,比如N1 = c2 x 264x2 + c1 x 264 + c0 ,则[]abs={c0, c1, c2}。这里有一个迭代,先是由{b0 、r1}=>{中64位,c0},尔后由{b1 、中64位}=>{c2 、c1}
…
添加过程就是原有系数“乘权重,加元素,低64位入切片”的循环迭代过程。挺啰嗦,但它把由高位开始的任意进制读串变成了低位开始的264 进制系数写入。
mulAddWW在返回时会将高位的0值系数去除,这样就不会出现前面提到的本应放在一个字内却用了两个字的问题。
- 收尾,将剩余不足n个数字的值按前法(权重有所变化)写入切片。
下面是汇编代码:
// func mulAddVWW(z, x []Word, y, r Word) (c Word)
// x为原系数切片,至少已有一个元素
// z是要生成的新切片,已基于x创建,长度相同,容量比x大1至4
// r为要添加的值
// y是权重
// c是计算后最高位的系数,不在这里写入z
TEXT ·mulAddVWW(SB),NOSPLIT,$-1
MOVQ z+0(FP), R10 // R10=新切片元素指针,即reflect.SliceHeader.Data
MOVQ x+24(FP), R8 // R8 =原切片元素指针
MOVQ y+48(FP), R9 // R9 =权重
MOVQ r+56(FP), CX // CX =要添加的值
MOVQ z_len+8(FP), R11 // R11=新切片长度,即reflect.SliceHeader.Len
MOVQ $0, BX // BX =0,循环计数,以下用i表示
CMPQ R11, $4
JL E5 // 新切片长度小于4
// 待处理元素数量>=4时,由U5过程处理,否则由L5处理,估计是为了提高缓存命中率
// 处理的过程基本相同
U5: MOVQ (0*8)(R8)(BX*8), AX
MULQ R9
ADDQ CX, AX
ADCQ $0, DX
MOVQ AX, (0*8)(R10)(BX*8)
MOVQ DX, CX
MOVQ (1*8)(R8)(BX*8), AX
MULQ R9
ADDQ CX, AX
ADCQ $0, DX
MOVQ AX, (1*8)(R10)(BX*8)
MOVQ DX, CX
MOVQ (2*8)(R8)(BX*8), AX
MULQ R9
ADDQ CX, AX
ADCQ $0, DX
MOVQ AX, (2*8)(R10)(BX*8)
MOVQ DX, CX
MOVQ (3*8)(R8)(BX*8), AX
MULQ R9
ADDQ CX, AX
ADCQ $0, DX
MOVQ AX, (3*8)(R10)(BX*8)
MOVQ DX, CX
ADDQ $4, BX // 已处理的i += 4
LEAQ 4(BX), DX // 测试 i+4 和 切片长度的大小
CMPQ DX, R11
JLE U5 // 还有4个以上待处理元素
JMP E5
L5: MOVQ (R8)(BX*8), AX // AX=原切片第i个元素值
MULQ R9 // (DX, AX) = x[i] * y
ADDQ CX, AX // (DX, AX) = x[i] * y + CX
ADCQ $0, DX
MOVQ AX, (R10)(BX*8) // 低64位写入新切片
MOVQ DX, CX // 高64位放入CX
ADDQ $1, BX // i++
E5: CMPQ BX, R11
JL L5 // i小于新切片长度
MOVQ CX, c+64(FP) // 将最终的高64位返回
RET
大数的显示
func (x *Int) String() string //以原生态的形式显示大数的符号和各个系数。
func (x *Int) Text(base int) string //以指定进制显示大数的数值形式。
big包中提供了很多大数计算的方法,如加减乘除、与或非等等。当然还有大浮点数、大有理数。
本文详细介绍了Golang中的大数表示方法,包括大数如何存储、初始化及显示。math/big包提供了解决大数运算的功能,通过将大数以2^64为基数展开并存储在切片中。同时,文章还讨论了如何初始化大数,并展示了将字符串转化为大数的实现过程,涉及到的mulAddWW和mulAddVWW等关键函数的解析。

2033

被折叠的 条评论
为什么被折叠?



