Why is factorial calculation much faster in Haskell than in Java

后端 未结 5 1915
终归单人心
终归单人心 2021-02-07 01:56

One of the programming problems I have come across involves calculation of factorials of large numbers (of numbers up to 10^5). I have seen a simple Haskell code which goes lik

5条回答
  •  孤城傲影
    2021-02-07 02:14

    The difference is, as shachaf said, that GHC (by default) uses GMP for Integer computations that exceed the Int range, and GMP is rather well optimised. It has nothing to do with purity, caching, tail-call optimisation or such.

    Java's BigInteger uses more or less the naive schoolbook algorithms. If you look at the code for multiply (openjdk7), the work horse is

    /**
     * Multiplies int arrays x and y to the specified lengths and places
     * the result into z. There will be no leading zeros in the resultant array.
     */
    private int[] multiplyToLen(int[] x, int xlen, int[] y, int ylen, int[] z) {
        int xstart = xlen - 1;
        int ystart = ylen - 1;
    
        if (z == null || z.length < (xlen+ ylen))
            z = new int[xlen+ylen];
    
        long carry = 0;
        for (int j=ystart, k=ystart+1+xstart; j>=0; j--, k--) {
            long product = (y[j] & LONG_MASK) *
                           (x[xstart] & LONG_MASK) + carry;
            z[k] = (int)product;
            carry = product >>> 32;
        }
        z[xstart] = (int)carry;
    
        for (int i = xstart-1; i >= 0; i--) {
            carry = 0;
            for (int j=ystart, k=ystart+1+i; j>=0; j--, k--) {
                long product = (y[j] & LONG_MASK) *
                               (x[i] & LONG_MASK) +
                               (z[k] & LONG_MASK) + carry;
                z[k] = (int)product;
                carry = product >>> 32;
            }
            z[i] = (int)carry;
        }
        return z;
    }
    

    a quadratic digit-by-digit multiplication (digits are of course not base 10). That doesn't hurt too much here, since one of the factors is always single-digit, but indicates that not too much work has yet been put into optimising BigInteger computations in Java.

    One thing that can be seen from the source is that in Java products of the form smallNumber * largeNumber are faster than largeNumber * smallNumber (in particular if the small number is single-digit, having that as the first number means the second loop with the nested loop doesn't run at all, so you have altogether less loop control overhead, and the loop that is run has a simpler body).

    So changing

    f = f.multiply(BigInteger.valueOf(i));
    

    in your Java version to

    f = BigInteger.valueOf(i).multiply(f);
    

    gives a considerable speedup (increasing with the argument, ~2× for 25000, ~2.5× for 50000, ~2.8× for 100000).

    The computation is still much slower than the GHC/GMP combination by a factor of roughly 4 in the tested range on my box, but, well, GMP's implementation is plain better optimised.

    If you make computations that often multiply two large numbers, the algorithmic difference between the quadratic BigInteger multiplication and GMP's that uses Karatsuba or Toom-Cook when the factors are large enough (FFT for really large numbers) would show.

    However, if multiplying is not all that you do, if you print out the factorials, hence convert them to a String, you get hit by the fact that BigInteger's toString method is abominably slow (it's roughly quadratic, so since the computation of the factorial is altogether quadratic in the length of the result, you get no [much] higher algorithmic complexity, but you get a big constant factor on top of the computation). The Show instance for Integer is much better, O(n * (log n)^x) [not sure what x is, between 1 and 2], so converting the result to String adds just a little to the computation time.

提交回复
热议问题