Java:如何优化大数组的总和

时间:2014-05-04 12:15:10

标签: java optimization

我尝试在codeforces上解决一个问题。我得到Time limit exceeded判决。唯一耗时的操作是大数组的计算和。所以我试图优化它,但没有结果。

我想要的是什么:优化下一个功能:

//array could be Integer.MAX_VALUE length
private long canocicalSum(int[] array) { 
    int sum = 0;
    for (int i = 0; i < array.length; i++)
        sum += array[i];
    return sum;
}

问题1 [主要]:是否可以优化canonicalSum

我已经尝试过以避免使用非常大的数字进行操作。所以我决定使用辅助数据。例如,我将array1[100]转换为array2[10],其中array2[i] = array1[i] + array1[i+1] + array1[i+9]

private long optimizedSum(int[] array, int step) {
    do {
        array = sumItr(array, step);
    } while (array.length != 1);
    return array[0];
}

private  int[] sumItr(int[] array, int step) {
    int length = array.length / step + 1;
    boolean needCompensation = (array.length % step == 0) ? false : true;
    int aux[] = new int[length];
    for (int i = 0, auxSum = 0, auxPointer = 0; i < array.length; i++) {
        auxSum += array[i];
        if ((i + 1) % step == 0) {
            aux[auxPointer++] = auxSum;
            auxSum = 0;
        }
        if (i == array.length - 1 && needCompensation) {
            aux[auxPointer++] = auxSum;
        }
    }
    return aux;
}

问题:但似乎canonicalSumoptimizedSum快十倍。在这里我的测试:

@Test
public void sum_comparison() {
    final int ARRAY_SIZE = 100000000;
    final int STEP = 1000;
    int[] array = genRandomArray(ARRAY_SIZE);

    System.out.println("Start canonical Sum");
    long beg1 = System.nanoTime();
    long sum1 = canocicalSum(array);
    long end1 = System.nanoTime();
    long time1 = end1 - beg1;
    System.out.println("canon:" + TimeUnit.MILLISECONDS.convert(time1, TimeUnit.NANOSECONDS) + "milliseconds");

    System.out.println("Start optimizedSum");
    long beg2 = System.nanoTime();
    long sum2 = optimizedSum(array, STEP);
    long end2 = System.nanoTime();
    long time2 = end2 - beg2;
    System.out.println("custom:" + TimeUnit.MILLISECONDS.convert(time2, TimeUnit.NANOSECONDS) + "milliseconds");

    assertEquals(sum1, sum2);
    assertTrue(time2 <= time1);
}

private int[] genRandomArray(int size) {
    int[] array = new int[size];
    Random random = new Random();
    for (int i = 0; i < array.length; i++) {
        array[i] = random.nextInt();
    }
    return array;
}

问题2:为什么optimizedSum的工作速度比canonicalSum慢?

3 个答案:

答案 0 :(得分:5)

从Java 9开始,此操作的矢量化为implementeddisabled,基于测量代码的全部成本及其编译的基准。根据您的处理器,这会产生相对有趣的结果,如果您introduce artificial complications进入还原循环,您可以触发自动向量化并获得更快的结果!因此,目前最快的代码,假设数字足够小而不会溢出,是:

public int sum(int[] data) {
    int value = 0;
    for (int i = 0; i < data.length; ++i) {
        value += 2 * data[i];
    }
    return value / 2;
}

这不是一个推荐!这更多地说明了Java中代码的速度取决于JIT,它的权衡以及任何给定版本中的错误/特性。编写可爱的代码来优化这样的问题至多是徒劳的,并且会给你编写的代码带来保质期。例如,如果您手动展开循环以针对较旧版本的Java进行优化,那么在Java 8或9中,您的代码会慢得多,因为此决定将完全禁用自动向量化。你最好真的需要这种表现才能做到。

答案 1 :(得分:4)

如果要添加N个数字,则运行时为O(N)。因此,在这方面,您的canonicalSum无法进行优化&#34; 你可以做些什么来减少运行时间使得求和并行。即将数组分解为部分并将其传递给单独的线程,最后将每个线程返回的结果相加 更新:这意味着多核系统,但有一个java api来获取核心数

答案 2 :(得分:4)

  

问题1 [主要]:是否可以优化canonicalSum?

是的,确实如此。但我不知道是什么因素。

您可以做的一些事情是:

  • 使用Java 8中引入的并行管道。处理器具有执行2个数组(以及更多)的并行求和的指令。当您使用“。+”(并行加法)或“+”求和两个向量时,可以在Octave中观察到这种情况。它比使用循环更快。

  • 使用多线程。你可以使用分而治之的算法。也许是这样的:

    • 将数组分成2个或更多
    • 继续递归分割,直到获得一个具有可管理大小的线程数组。
    • 开始使用单独的线程计算子数组(分割数组)的总和。
    • 最后将所有子阵列生成的总和(从所有线程中)添加到一起以产生最终结果
  • 也许展开循环也会有所帮助。通过循环展开,我的意思是通过手动在循环中执行更多操作来减少循环必须执行的步骤。

来自http://en.wikipedia.org/wiki/Loop_unwinding的示例:

for (int x = 0; x < 100; x++)
{
    delete(x);
}

变为

for (int x = 0; x < 100; x+=5)
{
    delete(x);
    delete(x+1);
    delete(x+2);
    delete(x+3);
    delete(x+4);
}

但如上所述,这必须谨慎和分析,因为JIT本身可能会进行这种优化。

可以看到多线程方法的数学运算实现here

在java 7中引入的 Fork / Join框架的示例实现基本上完成了上面的分而治之的算法:

public class ForkJoinCalculator extends RecursiveTask<Double> {

   public static final long THRESHOLD = 1_000_000;

   private final SequentialCalculator sequentialCalculator;
   private final double[] numbers;
   private final int start;
   private final int end;

   public ForkJoinCalculator(double[] numbers, SequentialCalculator sequentialCalculator) {
     this(numbers, 0, numbers.length, sequentialCalculator);
   }

   private ForkJoinCalculator(double[] numbers, int start, int end, SequentialCalculator sequentialCalculator) {
     this.numbers = numbers;
     this.start = start;
     this.end = end;
     this.sequentialCalculator = sequentialCalculator;
   }

   @Override
   protected Double compute() {
     int length = end - start;
     if (length <= THRESHOLD) {
         return sequentialCalculator.computeSequentially(numbers, start, end);
     }
     ForkJoinCalculator leftTask = new ForkJoinCalculator(numbers, start, start + length/2, sequentialCalculator);
     leftTask.fork();
     ForkJoinCalculator rightTask = new ForkJoinCalculator(numbers, start + length/2, end, sequentialCalculator);
     Double rightResult = rightTask.compute();
     Double leftResult = leftTask.join();
     return leftResult + rightResult;
  }
}
  

这里我们开发一个RecursiveTask分割一个双打数组,直到   子阵列的长度不会低于给定的阈值。在这   指出子阵列是按顺序处理的   由以下界面定义的操作

使用的界面是:

public interface SequentialCalculator {
  double computeSequentially(double[] numbers, int start, int end);
}

用法示例:

public static double varianceForkJoin(double[] population){
   final ForkJoinPool forkJoinPool = new ForkJoinPool();
   double total = forkJoinPool.invoke(new ForkJoinCalculator(population, new SequentialCalculator() {
     @Override
     public double computeSequentially(double[] numbers, int start, int end) {
       double total = 0;
       for (int i = start; i < end; i++) {
         total += numbers[i];
       }
       return total;
     }
  }));
  final double average = total / population.length;
  double variance = forkJoinPool.invoke(new ForkJoinCalculator(population, new SequentialCalculator() {
    @Override
    public double computeSequentially(double[] numbers, int start, int end) {
      double variance = 0;
      for (int i = start; i < end; i++) {
        variance += (numbers[i] - average) * (numbers[i] - average);
      }
      return variance;
    }
 }));
 return variance / population.length;
}