【问题标题】:BigInteger numbers implementation and performanceBigInteger 数字的实现和性能
【发布时间】:2014-07-20 23:10:11
【问题描述】:

我用 C++ 编写了一个 BigInteger 类,它应该能够对任何大小的所有数字进行运算。目前,我正在尝试通过比较现有算法并测试它们最有效的数字数量来实现一种非常快速的乘法方法,但我遇到了非常意想不到的结果。我尝试对 500 位数字进行 20 次乘法并计时。结果是这样的:

karatsuba:
  14.178 seconds

long multiplication:
  0.879 seconds

维基百科告诉我

因此,对于足够大的 n,Karatsuba 的算法将执行比普通乘法更少的移位和个位数加法,即使它的基本步骤比直接公式使用更多的加法和移位。然而,对于较小的 n 值,额外的移位和加法操作可能使其运行速度比普通方法慢。正回报点取决于计算机平台和环境。根据经验,当被乘数超过 320–640 位时,Karatsuba 通常会更快。

由于我的数字至少有 1500 位长,这是非常出乎意料的,因为维基百科说 karatsuba 应该运行得更快。我相信我的问题可能出在我的加法算法上,但我不知道如何让它更快,因为它已经在 O(n) 中运行。我将在下面发布我的代码,以便您检查我的实现。我会把不相关的部分排除在外。
我也在想,也许我使用的结构不是最好的。我用小端序表示每个数据段。因此,例如,如果我将数字 123456789101112 存储到长度为 3 的数据段中,它将如下所示:

{112,101,789,456,123}

所以这就是为什么我现在要问实现 BigInteger 类的最佳结构和最佳方法是什么?为什么 karatsuba 算法比长乘法慢?

这是我的代码:(我很抱歉长度)

using namespace std;

bool __longmult=true;
bool __karatsuba=false;

struct BigInt {
public:
    vector <int> digits;

    BigInt(const char * number) {
        //constructor is not relevant   
    }
    BigInt() {}

    void BigInt::operator = (BigInt a) {
        digits=a.digits;
    }

    friend BigInt operator + (BigInt,BigInt);
    friend BigInt operator * (BigInt,BigInt);

    friend ostream& operator << (ostream&,BigInt);
};

BigInt operator + (BigInt a,BigInt b) {
    if (b.digits.size()>a.digits.size()) {
        a.digits.swap(b.digits); //make sure a has more or equal amount of digits than b
    }
    int carry=0;

    for (unsigned int i=0;i<a.digits.size();i++) {
        int sum;
        if (i<b.digits.size()) {
            sum=b.digits[i]+a.digits[i]+carry;
        } else if (carry==1) {
            sum=a.digits[i]+carry;
        } else {
            break; // if carry is 0 and no more digits in b are left then we are done already
        }

        if (sum>=1000000000) {
            a.digits[i]=sum-1000000000;
            carry=1;
        } else {
            a.digits[i]=sum;
            carry=0;
        }
    }

    if (carry) {
        a.digits.push_back(1);
    }

    return a;
}

BigInt operator * (BigInt a,BigInt b) {
    if (__longmult) {
        BigInt res;
        for (unsigned int i=0;i<b.digits.size();i++) {
            BigInt temp;
            temp.digits.insert(temp.digits.end(),i,0); //shift to left for i 'digits'

            int carry=0;
            for (unsigned int j=0;j<a.digits.size();j++) {
                long long prod=b.digits[i];
                prod*=a.digits[j];
                prod+=carry;
                int t=prod%1000000000;
                temp.digits.push_back(t);
                carry=(prod-t)/1000000000;
            }
            if (carry>0) {
                temp.digits.push_back(carry);
            }
            res+=temp;
        }
        return res;
    } else if (__karatsuba) {
        BigInt res;
        BigInt a1,a0,b1,b0;
        assert(a.digits.size()>0 && b.digits.size()>0);
        while (a.digits.size()!=b.digits.size()) { //add zeroes for equal size
            if (a.digits.size()>b.digits.size()) {
                b.digits.push_back(0);
            } else {
                a.digits.push_back(0);
            }
        }

        if (a.digits.size()==1) {
            long long prod=a.digits[0];
            prod*=b.digits[0];

            res=prod;//conversion from long long to BigInt runs in constant time
            return res;

        } else {
            for (unsigned int i=0;i<a.digits.size();i++) {
                if (i<(a.digits.size()+(a.digits.size()&1))/2) { //split the number in 2 equal parts
                    a0.digits.push_back(a.digits[i]);
                    b0.digits.push_back(b.digits[i]);
                } else {
                    a1.digits.push_back(a.digits[i]);
                    b1.digits.push_back(b.digits[i]);
                }
            }
        }

        BigInt z2=a1*b1;
        BigInt z0=a0*b0;
        BigInt z1 = (a1 + a0)*(b1 + b0) - z2 - z0;

        if (z2==0 && z1==0) {
            res=z0;
        } else if (z2==0) {
            z1.digits.insert(z1.digits.begin(),a0.digits.size(),0);
            res=z1+z0;
        } else {
            z1.digits.insert(z1.digits.begin(),a0.digits.size(),0);
            z2.digits.insert(z2.digits.begin(),2*a0.digits.size(),0);
            res=z2+z1+z0;
        }

        return res;
    }
}

int main() {
    clock_t start, end;

    BigInt a("984561231354629875468546546534125215534125215634987498548489456125215421563498749854848945612385663498749854848945612521542156349874985484894561238561698774565123165221393856169877456512316552156349874985484894561238561698774565123165221392213935215634987498548489456123856169877456512316522139521563498749854848945612385616987745651231652213949651465123151354686324848945612385616987745651231652213949651465123151354684132319321005482265341252156349874985484894561252154215634987498548489456123856264596162131");
    BigInt b("453412521563498749853412521563498749854848945612521542156349874985484894561238565484894561252154215634987498548489456123856848945612385616935462987546854521563498749854848945653412521563498749854848945612521542156349874985484894561238561238754579785616987745651231652213965465341235215634987495215634987498548489456123856169877456512316522139854848774565123165223546298754685465465341235215634987498548354629875468546546534123521563498749854844139496514651231513546298754685465465341235215634987498548435468");

    __longmult=false;
    __karatsuba=true;

    start=clock();
    for (int i=0;i<20;i++) {
        a*b;
    }
    end=clock();
    printf("\nTook %f seconds\n", (double)(end-start)/CLOCKS_PER_SEC);

    __longmult=true;
    __karatsuba=false;

    start=clock();
    for (int i=0;i<20;i++) {
        a*b;
    }
    end=clock();
    printf("\nTook %f seconds\n", (double)(end-start)/CLOCKS_PER_SEC);

    return 0;
}

【问题讨论】:

  • 您似乎正在考虑将
  • 这正是我之前所做的。结果:在 16 秒内进行单个 500 位乘法...我相信使用
  • 您能否展示您的整个代码,以便有人可以试用。
  • 另外,根据您的系统,您可能会遇到 int 溢出。
  • 你应该使用分析器找出时间花在哪里

标签: c++ performance algorithm implementation biginteger


【解决方案1】:
  1. 你使用 std::vector

    对于您的数字,请确保其中没有不必要的重新分配。所以在操作之前分配空间以避免它。另外我不使用它,所以我不知道数组范围检查速度是否下降。

    检查你是否不移动它!这是O(N) ...即插入到第一个位置...

  2. 优化您的实施

    here 你可以找到我的实现优化和未优化的比较

    x=0.98765588997654321000000009876... | 98*32 bits...
    mul1[ 363.472 ms ]... O(N^2) classic multiplication
    mul2[ 349.384 ms ]... O(3*(N^log2(3))) optimized karatsuba multiplication
    mul3[ 9345.127 ms]... O(3*(N^log2(3))) unoptimized karatsuba multiplication 
    

    Karatsuba 的我的实现阈值约为 3100 位 ... ~ 944 位!!!情人阈值越优化的代码。


    尝试从函数操作数中删除不必要的数据

    //BigInt operator + (BigInt a,BigInt b)
    BigInt operator + (const BigInt &a,const BigInt &b)
    

    这样您就不会在每个 + 调用中在堆上创建另一个 a,b 副本,这样也更快:

    mul(BigInt &ab,const BigInt &a,const BigInt &b) // ab = a*b
    
  3. Schönhage-Strassen 乘法

    这是基于 FFTNTT 的。我的阈值很大......〜49700bits ...〜15000digits所以如果你不打算使用这么大的数字,那就忘了它。实现也在上面的链接中。


    here 是我的 NTT 实现(尽我所能优化)

  4. 总结

    使用小端或大端无关紧要,但您应该以不使用插入操作的方式对操作进行编码。


    您对速度较慢的数字使用十进制基数,因为您需要使用除法和模运算。如果你选择 base 作为 2 的幂,那么只需位操作就足够了并且它还从代码中删除了许多最慢的 if 语句。如果您需要基数为 10 的幂,则在某些情况下使用最大的,这样可以将 div、mod 减少到很少的减法

    2^32 = 4 294 967 296 ... int = +/- 2147483648
    base = 1 000 000 000
    
    //x%=base
    while (x>=base) x-=base;
    

    在某些平台上,最大周期数是 2^32/base 或 2^31/base,这比取模更快,而且基数越大,您需要的操作就越少,但要注意溢出!!!

【讨论】:

  • 所以你会建议使用vector &lt;bool&gt; digits 而不是vector &lt;int&gt; digits; 并将其用作二进制表示?
  • @gelatin1 不,你仍然可以使用向量,但基数不是 1000000000,而是 0x100000000,你唯一需要添加的是十进制和十六进制字符串之间的转换,如下所示:stackoverflow.com/a/18231860/2521214I更喜欢十六进制表示,因为它对于打印甚至分配操作要快得多(没有除法,只是位操作)。当然,对于大数字,这种转换比对它们本身的操作要慢,所以要小心你测量的时间
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2023-03-16
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多