go math/big:提高nat.mulRange的性能

rqqzpn5f  于 22天前  发布在  Go
关注(0)|答案(5)|浏览(16)

2024年1月7日,星期日,下午5:46,John Jannotti < jannotti@gmail.com > 写道:

我很喜欢大数实现,所以我在浏览nat.go时发现mulRange以一种令人惊讶的递归方式实现。在非基本情况下,mulRange(a, b)返回mulrange(a, (a+b)/2) * mulRange(1+(a+b)/2, b)(许多big.Int仪式被省略)。
这没问题,但我没有看到它比简单直接的for循环有什么优势。

z = z.setUint64(a)
for m := a + 1; m <= b; m++ {
	z = z.mul(z, nat(nil).setUint64(m))
}
return z

实际上,我怀疑现有的代码运行速度更慢,分配了更多的内存。事实证明这是正确的。使用现有的单元测试作为基准进行快速基准测试,结果如下:
BenchmarkRecusiveMulRangeN-10 169417 6856 ns/op 9452 B/op 338 allocs/op
BenchmarkIterativeMulRangeN-10 265354 4269 ns/op 2505 B/op 196 allocs/op
我怀疑mulRange不是任何人代码中的性能瓶颈!但是它被导出为int.MulRange,所以我想它有一定的价值。鉴于for循环似乎更容易理解递归版本,也许值得提交一个PR?(如果是这样,我应该先创建一个问题吗?)

yxyvkwin

yxyvkwin1#

应该可以通过预先分配接近正确数量的空间,然后使用 mulAddVWW 迭代计算乘积来实现 mulRange。

s71maibg

s71maibg2#

由于nat.mul具有优化调用z.mulAddWW的功能,当y只有一个单词时,以下分配仅执行一次(在64位平台上)。

func (z nat) mulRangeIterative(a, b uint64) nat {
	switch {
	case a == 0:
		return z.setUint64(0)
	case a > b:
		return z.setUint64(1)
	}
	maxBits := uint64(bits.Len64(b)) * (b - a + 1)
	maxWords := (maxBits + _W - 1) / _W
	z = z.make(int(maxWords))
	z = z.setUint64(a)

	var buf [2]Word
	mb := nat(buf[:])
	for m := b; m > a; m-- {
		mb = mb.setUint64(m)
		z = z.mul(z, mb)
	}
	return z
}

maxBits将在类似mulRange(0, math.MaxUInt64)的病态输入下溢出。这种溢出不会影响正确性(如果它能完成),它只会启动z太小。当然,当它试图增长到足够大时,它会失败。
基准:

func BenchmarkMulRangeNRecursive(b *testing.B) {
	b.ReportAllocs()
	for n := 0; n < b.N; n++ {
		r := mulRangesN[n%len(mulRangesN)]
		nat(nil).mulRange(r.a, r.b)
	}
}

func BenchmarkMulRangeNIterative(b *testing.B) {
	b.ReportAllocs()
	for n := 0; n < b.N; n++ {
		r := mulRangesN[n%len(mulRangesN)]
		nat(nil).mulRangeIterative(r.a, r.b)
	}
}

产生

BenchmarkMulRangeNRecursive-10    	 3400255	       348.8 ns/op	     589 B/op	      21 allocs/op
BenchmarkMulRangeNIterative-10    	11322468	       104.6 ns/op	      33 B/op	       1 allocs/op
332nm8kg

332nm8kg3#

Bakul Shah的评论bakul@iitbombay.org引导我调查了非常大的输入。
通过在递归的每一侧建立大值,Karatsuba用于较大的乘法运算,递归版本开始获胜。在mulRange(1_000, 200_000)上,它比单分配迭代版本快20多倍。
我将编写一个混合版本,使用迭代方法处理较短的范围,这应该让我们两者兼得。

ivqmmu1c

ivqmmu1c4#

参考:https://groups.google.com/g/golang-nuts/c/7kcFb41ARgM/m/5sSxZlKfAgAJ
这可以很容易地并行化,尽管可能不值得,除非用于特定的应用。
一些阶乘的背景和其他有趣的算法:http://www.luschny.de/math/factorial/FastFactorialFunctions.htm
Richard Fateman的CL实现:https://people.eecs.berkeley.edu/~fateman/papers/factorial.pdf

7ivaypg9

7ivaypg95#

我有一个简单的混合实现,如下所示:

func (z nat) mulRange(a, b uint64) nat {
	maxBits := uint64(bits.Len64(b)) * (b - a + 1)
	maxWords := (maxBits + _W - 1) / _W
	// use recursive algorithm if it looks like it will result in each side being
	// big enough for karatsuba.
	if maxWords > uint64(2*karatsubaThreshold) {
		return z.mulRangeRecursive(a, b)
	}
	return z.mulRangeIterative(a, b)
}

不幸的是,截止值 > 2 * karatsubaThreshold 有点低。在一个基准测试中,ab 之间的范围越来越大,在截止值上方有一个略慢的窗口大小。因此,截止值应该更高。然而,我不确定将其硬编码到 calibration_test.go 中是否值得。
一个由手工调整确定的“魔术”截止值,如 maxWords > uint64(5*karatsubaThreshold/2) 或类似的东西,是否可以接受,还是你想看到更严格的内容?

相关问题