golang的大数表示

本文详细介绍了Golang中的大数表示方法,包括大数如何存储、初始化及显示。math/big包提供了解决大数运算的功能,通过将大数以2^64为基数展开并存储在切片中。同时,文章还讨论了如何初始化大数,并展示了将字符串转化为大数的实现过程,涉及到的mulAddWW和mulAddVWW等关键函数的解析。

大数表示

  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()的返回值也没有问题。这个方法的实现有点意思,为了效率使用了汇编语言,下面作一简要剖析。

  1. 将字符串s转化为一个字节流。
  2. 确定符号,然后把后续的字节流都当成无符号数来处理,即绝对值。
  3. 确定进制,如是否为0、0x、0b先导等等。
  4. 根据进制,确定一个字长能够容纳的该进制数字的个数n和基本权重bn,比如:64位字长可容纳的最大十进制数为1.8447x1019,共20个数字,这里取n=19,基本权重为1019。因为如果取20可能会“冒顶”,如2x1019也是20个数字但放不进一个字长内。所以这个n实际上是该进制能安全地存入一个字长的最大数字个数。至于基本权重将用于后续进制转换计算。从这里还能预感到一个问题,比如1x1019虽然是20个数字,但能放在一个字长内,golang是不是用了两个字长?
  5. 从现在开始循环读入字节流,每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值系数去除,这样就不会出现前面提到的本应放在一个字内却用了两个字的问题。
  1. 收尾,将剩余不足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包中提供了很多大数计算的方法,如加减乘除、与或非等等。当然还有大浮点数、大有理数。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值