Mar*_*ott 10 c++ gmp fibonacci
我最近为了自己的娱乐而研究和测试各种Fibonacci算法,或多或少意外地提出了经典的O(n)时间和O(1)空间动态编程实现的替代实现.
考虑以下两个功能:
BigInt fib_dp_classic(int n) {
if (n == 0) {
return 0;
}
BigInt x = 0, y = 1, z;
for (int i = 2; i <= n; ++i) {
z = x + y;
x = y;
y = z;
}
return y;
}
Run Code Online (Sandbox Code Playgroud)
和
BigInt fib_dp_mod(int n) {
BigInt x = 0, y = 1, z = 1;
for (int i = 0; i < n; ++i) {
switch (i % 3) {
case 0:
y = x + z;
break;
case 1:
z = x + y;
break;
case 2:
x = y + z;
break;
}
}
switch (n % 3) {
case 0:
return x;
break;
case 1:
return y;
break;
case 2:
return z;
break;
}
}
Run Code Online (Sandbox Code Playgroud)
在我的机器上,使用fib_dp_classic计算百万分之斐波纳契数需要6.55秒,使用fib_dp_mod计算百万分之2.83秒,甚至打开-O3也不会改变太多.关于为什么mod版本更快,我真的没有任何好主意.是因为经典版本中的额外商店说明比第二版中的mod贵吗?我的理解是编译器应该将所有三个变量放在两个版本的寄存器中,并且计算mod实际上相当昂贵; 情况并非如此吗?
实际上,我只是通过编译器资源管理器将这两者都放在一起,并且一旦你开启优化,它们都只使用寄存器.当然,这只是使用整数,而不是我实际用于基准测试的基于GMP的重点.是否有一些奇怪的GMP实施细节可能是这里的原因?
更新:我甚至试图看看malloc()是否是罪魁祸首,而fib_dp_classic使用130个系统调用(对于n = 1000000)而fib_dp_mod使用133.所以仍然没有真正的线索......
更新2:是的,缓冲区副本是罪魁祸首(正如geza所指出的那样)而我却因为没有意识到而愚蠢.以下是两个备用版本及其基准测试结果:
BigInt fib_dp_move(int n) {
if (n == 0) {
return 0;
}
BigInt x = 0, y = 1, z;
for (int i = 2; i <= n; ++i) {
z = std::move(x) + y;
x = std::move(y);
y = std::move(z);
}
return y;
}
Run Code Online (Sandbox Code Playgroud)
这在2.84秒内运行,因此它大约相当于mod版本,因为它消除了不必要的副本.
BigInt fib_dp_swap(int n) {
if (n == 0) {
return 0;
}
BigInt x = 0, y = 1, z;
for (int i = 2; i <= n; ++i) {
z = x + y;
swap(x, y);
swap(y, z);
}
return y;
}
Run Code Online (Sandbox Code Playgroud)
这(从格扎)也运行在2.84秒,如此反复约等同于MOD版,因为它消除了在复制方式基本相同,只是打电话swap(),而不是使用移动语义.
在这种情况下,将 GMP 版本与 simple int 版本进行比较是没有意义的。Fib(1000000) 是一个约 86KB 的数字,它比简单的int. 对于ints来说,在某些情况下复制基本上可以是自由操作,而对于GMP号码来说,则涉及到86KB缓冲区的复制。
(当然,请注意,副本不会总是 86KB。一开始,它是 ~0KB,但随着例程的进行,它会增长到 86KB。并且随着数字的增长,例程变得越来越慢,所以大部分时间都花在数字很大的时候)
假设实现质量良好BigInt,我们在每次迭代中都有以下操作计数:
正如您所看到的,经典版本有 2 个额外的副本(注意,在高质量实现中,x=y+z不涉及副本)。并且复制的速度与添加的速度具有相同的数量级(我的意思是,添加可能比复制慢 2-3 倍)。因此,这解释了经典例程减速约 2.3 倍的原因。
请注意,如果BigInt是使用引用计数的实现,则x=y操作基本上可以是自由操作,因为它不需要副本,只需递增计数器(在这种情况下,经典例程将具有与模式一)。
最后注意:如果非复制swap操作可用,您可能可以加快经典版本的速度:
BigInt fib_dp_swap(int n) {
if (n == 0) {
return 0;
}
BigInt x = 0, y = 1, z;
for (int i = 2; i <= n; ++i) {
z = x + y;
x.swap(y); // Note the 2 swaps here
y.swap(z);
}
return y;
}
Run Code Online (Sandbox Code Playgroud)
使用gmpxx,这个例程与mod版本同时运行,并且简单得多。
这是因为swap操作比副本便宜得多。Swap 只需要交换内部的指针BigInt,而副本则需要约 86KB 的内存副本。