Karatsuba乘法改进

Karatsuba multiplication improvement

本文关键字:Karatsuba      更新时间:2023-10-16

我实现了Karatsuba乘法算法为我的教育目标。现在我正在寻找进一步的改进。我已经实现了某种长算法,它工作得很好,我是否不使用整数表示的基数超过100。当基数10时,clang++ -O3编译[10^50000, 10^50001]范围内的两个随机整数相乘,取:

Naive algorithm took me 1967 cycles (1.967 seconds)
Karatsuba algorithm took me 400 cycles (0.4 seconds)

和基数100的相同数字:

Naive algorithm took me 409 cycles (0.409 seconds)
Karatsuba algorithm took me 140 cycles (0.14 seconds)

有办法改善这个结果吗?现在我使用这样的函数来完成我的结果:

void finalize(vector<int>& res) {
    for (int i = 0; i < res.size(); ++i) {
        res[i + 1] += res[i] / base;
        res[i] %= base;
    }
}

正如你所看到的,每一步它都会计算进位并将其压入下一位数字。如果我取碱>=1000,结果将溢出。

如果你看到我的代码,我用int的向量来表示长整数。根据我的底数,一个数字会被分成矢量的不同部分。现在我看到了几个选项:

  • 使用long long类型作为向量,但是对于大长度整数
  • 也可能溢出。
  • 实现长算术进位的表示

在我看到一些评论后,我决定扩大这个问题。假设我们想要将长整型表示为int型的向量。instanse:

ULLONG_MAX = 18446744073709551615

作为输入,我们传递第210个斐波那契数34507973060837282187130139035400899082304280,它不适合任何标准类型。如果我们把它表示成以10000000为基数的int型向量,它就像:

v[0]: 2304280
v[1]: 89908
v[2]: 1390354
v[3]: 2187130
v[4]: 6083728
v[5]: 5079730
v[6]: 34

当我们做乘法运算时,我们可以得到(为了简单起见,假设是两个相同的数)(34507973060837282187130139035400899082304280)^2:

v[0] * v[0] = 5309706318400
...
v[0] * v[4] = 14018612755840
...

这只是第一行,我们必须这样做六个步骤。当然,某些步骤会在乘法运算或进位运算之后导致溢出。

如果我遗漏了什么,请告诉我,我会修改的。如果你想看完整版本,它在我的github

进制2^64和进制2^32是进行高精度运算最常用的进制。通常,数字存储在无符号整型中,因为它们在溢出方面具有良好的语义。

例如,可以这样检测加法的进位:

uint64_t x, y; // initialize somehow
uint64_t sum = x + y;
uint64_t carry = sum < x; // 1 if true, 0 if false

同时,汇编语言通常有一些"add with carry"指令;如果你能编写内联汇编(或者可以访问内部函数),你就可以利用这些。

对于乘法,大多数计算机都有可以计算一个机器字->两个机器字乘积的机器指令;有时,得到两半的指令被称为"高乘"answers"低乘"。你需要编写汇编来获得它们,尽管许多编译器提供了更大的整数类型,使用它们可以让你访问这些指令:例如,在gcc中,你可以实现multiply hi作为

uint64_t mulhi(uint64_t x, uint64_t y)
{
    return ((__uint128_t) x * y) >> 64;
}

当人们不能使用这个时,他们在2^32中做乘法,这样他们就可以用同样的方法实现一个可移植的多hi指令,使用uint64_t作为两位数类型。

如果你想编写高效的代码,你真的需要利用这些更大的乘法指令。以2^32为基数的数字相乘功能比以10为基数的数字相乘功能强大90倍以上。以2^64为基数进行数字乘法运算的能力是它的四倍。你的电脑可能比你用10为基数的乘法运算要快。