2016-01-27 1 views
4

Я создал модуль, который выполняет умножение матриц с использованием потоков. Это можно найти здесь: https://github.com/firefly-math/firefly-math-linear-real/Java 8 Stream Matrix Multiplication 10X медленнее, чем для Loop?

Я попытался написать контрольный тест, чтобы сравнить реализацию цикла потока с соответствующей реализацией цикла в Apache Commons Math.

Эталонный модуль находится здесь: https://github.com/firefly-math/firefly-math-benchmark

И фактический тест здесь: https://github.com/firefly-math/firefly-math-benchmark/blob/master/src/main/java/com/fireflysemantics/benchmark/MultiplyBenchmark.java

Когда я запускаю тест на матрицах размером, 100x100 и 1000x1000 оказывается, что Apache Commons Math (Использует для цикл) на 10 раз быстрее (грубо), чем соответствующая реализация потока.

# Run complete. Total time: 00:14:10 

Benchmark        Mode Cnt  Score  Error  Units 
MultiplyBenchmark.multiplyCM1000_1000 avgt 30 1040.804 ± 11.796 ms/op 
MultiplyBenchmark.multiplyCM100_100 avgt 30  0.790 ± 0.010 ms/op 
MultiplyBenchmark.multiplyFM1000_1000 avgt 30 11981.228 ± 405.812 ms/op 
MultiplyBenchmark.multiplyFM100_100 avgt 30  7.224 ± 0.685 ms/op 

Я сделал что-то не так в бенчмарке (надеюсь? :))?

Я добавляю проверенные методы таким образом, чтобы каждый мог видеть, что сравнивается. Это метод Apache Commons Math Array2DRowRealMatrix.multiply():

/** 
* Returns the result of postmultiplying {@code this} by {@code m}. 
* 
* @param m matrix to postmultiply by 
* @return {@code this * m} 
* @throws DimensionMismatchException if 
* {@code columnDimension(this) != rowDimension(m)} 
*/ 
public Array2DRowRealMatrix multiply(final Array2DRowRealMatrix m) 
    throws DimensionMismatchException { 
    MatrixUtils.checkMultiplicationCompatible(this, m); 

    final int nRows = this.getRowDimension(); 
    final int nCols = m.getColumnDimension(); 
    final int nSum = this.getColumnDimension(); 

    final double[][] outData = new double[nRows][nCols]; 
    // Will hold a column of "m". 
    final double[] mCol = new double[nSum]; 
    final double[][] mData = m.data; 

    // Multiply. 
    for (int col = 0; col < nCols; col++) { 
     // Copy all elements of column "col" of "m" so that 
     // will be in contiguous memory. 
     for (int mRow = 0; mRow < nSum; mRow++) { 
      mCol[mRow] = mData[mRow][col]; 
     } 

     for (int row = 0; row < nRows; row++) { 
      final double[] dataRow = data[row]; 
      double sum = 0; 
      for (int i = 0; i < nSum; i++) { 
       sum += dataRow[i] * mCol[i]; 
      } 
      outData[row][col] = sum; 
     } 
    } 

    return new Array2DRowRealMatrix(outData, false); 
} 

И это соответствующая реализация потока:

/** 
* Returns a {@link BinaryOperator} that multiplies {@link SimpleMatrix} 
* {@code m1} times {@link SimpleMatrix} {@code m2} (m1 X m2). 
* 
* Example {@code multiply(true).apply(m1, m2);} 
* 
* @param parallel 
*   Whether to perform the operation concurrently. 
* 
* @throws MathException 
*    Of type {@code MATRIX_DIMENSION_MISMATCH__MULTIPLICATION} if 
*    {@code m} is not the same size as {@code this}. 
* 
* @return the {@link BinaryOperator} that performs the operation. 
*/ 
public static BinaryOperator<SimpleMatrix> multiply(boolean parallel) { 

    return (m1, m2) -> { 
     checkMultiplicationCompatible(m1, m2); 

     double[][] a1 = m1.toArray(); 
     double[][] a2 = m2.toArray(); 

     Stream<double[]> stream = Arrays.stream(a1); 
     stream = parallel ? stream.parallel() : stream; 

     final double[][] result = 
       stream.map(r -> range(0, a2[0].length) 
         .mapToDouble(i -> range(0, a2.length).mapToDouble(j -> r[j] 
           * a2[j][i]).sum()) 
         .toArray()).toArray(double[][]::new); 

     return new SimpleMatrix(result); 
    }; 
} 

ТИА, Оле

+0

Есть ли разница, как в интерпретируемых языках, между явными циклами и неявными циклами, скрытыми «apply»? Является ли компилятор способным оптимизировать все вызовы, используемые при реализации функции «apply»? Звонки на (виртуальные?) Функции дороги. – LutzL

+1

@Holger 'toArray' является простым полевым аксессуаром. Я получаю [аналогичные результаты после упрощения теста] (https://bitbucket.org/assylias/performance/src/master/src/main/java/com/assylias/performance/SO35037893.java). Мое предположение - локальность данных и промахи кэша - может быть больше ... – assylias

+0

Метод toArray() в реализации потока возвращает ссылку на массив double [] [], прикрепленную к SimpleMatrix. Его легко можно было бы назвать getData(). Просто хочу дать понять, что на самом деле он не выполняет сопоставление от представления к другому. Также в целом обе реализации получают доступ к double [] []. Основное различие заключается в том, что потоки против не потоков. Даже при параллельном включении цикл for в два раза быстрее. – Ole

ответ

1

Взгляните на DoublePipeline.toArray:

public final double[] toArray() { 
    return Nodes.flattenDouble((Node.OfDouble) evaluateToArrayNode(Double[]::new)) 
        .asPrimitiveArray(); 
} 

Кажется, что в коробке сначала создается массив, который затем преобразуется в примитивный массив.

+2

Кажется, что эта функция существует только для выполнения внутреннего интерфейса (или, возможно, будет использоваться при вызове 'boxed()'). При выполнении пошаговой отладки вы увидите, что этот генератор не используется в этом случае. Возвращенный узел будет содержать массив 'double []', и поскольку этот поток имеет фиксированный размер, 'asPrimitiveArray()' будет напрямую возвращать его. – Holger

+0

@Holger Спасибо за обновление. Я посмотрю на него и удалю ответ, если он ошибается. Возможно, замедление вызвано большим количеством потоков, созданных тогда. – zeroflagL

+0

Ответ на самом деле неверен. После этого этот генератор не используется. Он передан 'AP :: valuToArrayNode' ->' AP :: evaluation' -> 'DP :: makeNodeBuilder', где он просто игнорируется. –