用于转置double [] [] Matrix的紧凑流表达式

时间:2016-01-18 18:20:30

标签: java math java-8 java-stream

我希望将double[][]矩阵转换成最紧凑,最有效的表达式。现在我有这个:

public static Function<double[][], double[][]> transpose() {
    return (m) -> {
        final int rows = m.length;
        final int columns = m[0].length;
        double[][] transpose = new double[columns][rows];
        range(0, rows).forEach(r -> {
            range(0, columns).forEach(c -> {
                transpose[c][r] = m[r][c];
            });
        });
        return transpose;
    };
}

思想?

5 个答案:

答案 0 :(得分:4)

你可以:

public static UnaryOperator<double[][]> transpose() {
    return m -> {
        return range(0, m[0].length).mapToObj(r ->
            range(0, m.length).mapToDouble(c -> m[c][r]).toArray()
        ).toArray(double[][]::new);
    };
}

此代码不使用forEach,但更喜欢mapToObjmapToDouble将每行映射到其转置。我还将Function<double[][], double[][]>更改为UnaryOperator<double[][]>,因为返回类型相同。

然而,像assylias的回答一样,使用简单的for循环可能会更有效率。

示例代码:

public static void main(String[] args) {
    double[][] m = { { 2, 3 }, { 1, 2 }, { -1, 1 } };
    double[][] tm = transpose().apply(m);
    System.out.println(Arrays.deepToString(tm)); // prints [[2.0, 1.0, -1.0], [3.0, 2.0, 1.0]]
}

我已经实现了一个JMH基准测试,比较上面的代码,for循环版本和上面的代码并行运行。使用大小为100,1000和3000的随机平方矩阵调用所有三种方法。结果是对于小矩阵,for循环版本更快但是对于更大的矩阵,并行流解决方案在性能方面确实更好( Windows 10,JDK 1.8.0_66,i5-3230M @ 2.60 GHz ):

Benchmark                           (matrixSize)  Mode  Cnt    Score    Error  Units
StreamTest.forLoopTranspose                  100  avgt   30    0,026 ±  0,001  ms/op
StreamTest.forLoopTranspose                 1000  avgt   30   14,653 ±  0,205  ms/op
StreamTest.forLoopTranspose                 3000  avgt   30  222,212 ± 11,449  ms/op
StreamTest.parallelStreamTranspose           100  avgt   30    0,113 ±  0,007  ms/op
StreamTest.parallelStreamTranspose          1000  avgt   30    7,960 ±  0,207  ms/op
StreamTest.parallelStreamTranspose          3000  avgt   30  122,587 ±  7,100  ms/op
StreamTest.streamTranspose                   100  avgt   30    0,040 ±  0,003  ms/op
StreamTest.streamTranspose                  1000  avgt   30   14,059 ±  0,444  ms/op
StreamTest.streamTranspose                  3000  avgt   30  216,741 ±  5,738  ms/op

基准代码:

@Warmup(iterations = 10, time = 500, timeUnit = TimeUnit.MILLISECONDS)
@Measurement(iterations = 10, time = 500, timeUnit = TimeUnit.MILLISECONDS)
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.MILLISECONDS)
@Fork(3)
public class StreamTest {

    private static final UnaryOperator<double[][]> streamTranspose() {
        return m -> {
            return range(0, m[0].length).mapToObj(r ->
                range(0, m.length).mapToDouble(c -> m[c][r]).toArray()
            ).toArray(double[][]::new);
        };
    }

    private static final UnaryOperator<double[][]> parallelStreamTranspose() {
        return m -> {
            return range(0, m[0].length).parallel().mapToObj(r ->
                range(0, m.length).parallel().mapToDouble(c -> m[c][r]).toArray()
            ).toArray(double[][]::new);
        };
    }

    private static final Function<double[][], double[][]> forLoopTranspose() {
        return m -> {
            final int rows = m.length;
            final int columns = m[0].length;
            double[][] transpose = new double[columns][rows];
            for (int r = 0; r < rows; r++)
                  for (int c = 0; c < columns; c++)
                    transpose[c][r] = m[r][c];
            return transpose;
        };
    }

    @State(Scope.Benchmark)
    public static class MatrixContainer {

        @Param({ "100", "1000", "3000" })
        private int matrixSize;

        private double[][] matrix;

        @Setup(Level.Iteration)
        public void setUp() {
            ThreadLocalRandom random = ThreadLocalRandom.current();
            matrix = random.doubles(matrixSize).mapToObj(i -> random.doubles(matrixSize).toArray()).toArray(double[][]::new);
        }

    }

    @Benchmark
    public double[][] streamTranspose(MatrixContainer c) {
        return streamTranspose().apply(c.matrix);
    }

    @Benchmark
    public double[][] parallelStreamTranspose(MatrixContainer c) {
        return parallelStreamTranspose().apply(c.matrix);
    }

    @Benchmark
    public double[][] forLoopTranspose(MatrixContainer c) {
        return forLoopTranspose().apply(c.matrix);
    }

}

答案 1 :(得分:3)

紧凑而高效:

for (int r = 0; r < rows; r++)
  for (int c = 0; c < cols; c++)
    transpose[c][r] = m[r][c];

请注意,如果您的Matrix类包含double[][],则另一种选择是返回具有相同基础数组但交换列/行索引的视图。您可以节省复制,但由于缓存局部性较差,您可能会在迭代时获得更差的性能。

答案 2 :(得分:2)

如果您假设一个矩形输入(原始代码似乎依赖),您可以将其写为

public static Function<double[][], double[][]> transpose() {
    return m -> range(0, m[0].length)
        .mapToObj(c->range(0, m.length).mapToDouble(r->m[r][c]).toArray())
        .toArray(double[][]::new);
}

这可以并行运行,但我想你需要一个该死的大矩阵来获得它的好处。

答案 3 :(得分:2)

我的建议:对于简单的低级数学,你应该使用普通的旧for循环而不是Stream API。此外,您应该非常仔细地对这些代码进行基准测试。

至于@Tunaki基准。首先,您不应该限制1微秒的单次测量。 matrixSize = 100的结果是完整垃圾:0,093 ± 0,0540,237 ± 0,134:错误超过50%。请注意,在每次迭代之前和之后执行的时间测量不是神奇的,也需要时间。这样一个小间隔很容易被一些突然醒来的Windows服务所破坏,需要一些CPU周期来检查一些东西,然后又重新入睡。我通常将每个预热/测量时间设置为500毫秒,这个数字对我来说很舒服。

其次,在使用非常简单的有效负载测试Stream API时(例如将数字复制到原始数组),您应该始终使用类型配置文件污染进行测试,因为它确实很重要。在干净的基准测试中,JIT编译器可以将所有内容都内联到单个方法中,因为它知道,例如,在一些range之后,您总是使用相同的lambda表达式调用相同的mapToObj。但在实际应用中它并不相同。我用这种方式修改了MatrixContainer类:

@State(Scope.Benchmark)
public static class MatrixContainer {
    @Param({"true", "false"})
    private boolean pollute;

    @Param({ "100", "1000", "3000" })
    private int matrixSize;

    private double[][] matrix;

    @Setup(Level.Iteration)
    public void setUp() {
        ThreadLocalRandom random = ThreadLocalRandom.current();
        matrix = random.doubles(matrixSize)
                       .mapToObj(i -> random.doubles(matrixSize).toArray())
                       .toArray(double[][]::new);
        if(!pollute) return;
        // do some seemingly harmless operations which will
        // poison JIT compiler type profile with some other lambdas
        for(int i=0; i<100; i++) {
           range(0, 1000).map(x -> x+2).toArray();
           range(0, 1000).map(x -> x+5).toArray();
           range(0, 1000).mapToObj(x -> x*2).toArray();
           range(0, 1000).mapToObj(x -> x*3).toArray();
        }
    }
}

此外,我设置了5个分叉,因为Stream API JIT编译器的行为可能与运行不同。编译进入后台线程,并且由于种族可能会在编译点处有不同的分析信息,这可能会显着改变编译结果。因此,在fork中,结果将是相同的,但在它们之间它们可能完全不同。

我的结果是(Windows 7,Oracle JVM 8u45 64位,一些不是很新的i5-2410笔记本电脑):

Benchmark                    (matrixSize)  (pollute)  Mode  Cnt    Score    Error  Units
StreamTest.forLoopTranspose           100       true  avgt   50    0,033 ±  0,001  ms/op
StreamTest.forLoopTranspose           100      false  avgt   50    0,032 ±  0,001  ms/op
StreamTest.forLoopTranspose          1000       true  avgt   50   17,094 ±  0,060  ms/op
StreamTest.forLoopTranspose          1000      false  avgt   50   17,065 ±  0,080  ms/op
StreamTest.forLoopTranspose          3000       true  avgt   50  260,173 ±  7,855  ms/op
StreamTest.forLoopTranspose          3000      false  avgt   50  258,774 ±  7,557  ms/op
StreamTest.streamTranspose            100       true  avgt   50    0,096 ±  0,001  ms/op
StreamTest.streamTranspose            100      false  avgt   50    0,055 ±  0,012  ms/op
StreamTest.streamTranspose           1000       true  avgt   50   21,497 ±  0,439  ms/op
StreamTest.streamTranspose           1000      false  avgt   50   15,883 ±  0,265  ms/op
StreamTest.streamTranspose           3000       true  avgt   50  272,806 ±  8,534  ms/op
StreamTest.streamTranspose           3000      false  avgt   50  260,515 ±  9,159  ms/op

现在你的错误少得多,并且看到类型污染会使流结果变差,同时不会影响循环结果。对于像100x100这样的矩阵,差异非常大。

答案 4 :(得分:0)

我添加了一个包含并行交换机的实现示例。我很好奇你们都在想什么。

/**
 * Returns a {@link UnaryOperator} that transposes the matrix.
 * 
 * Example {@code transpose(true).apply(m);}
 * 
 * @param parallel
 *            Whether to perform the transpose concurrently.
 */
public static UnaryOperator<ArrayMatrix> transpose(boolean parallel) {
    return (m) -> {
        double[][] data = m.getData();
        IntStream stream = range(0, m.getColumnDimension());
        stream = parallel ? stream.parallel() : stream;

        double[][] transpose =
                stream.mapToObj(
                        column -> range(0, data.length).mapToDouble(row -> data[row][column]).toArray())
                        .toArray(double[][]::new);
        return new ArrayMatrix(transpose);
    };
}