ParallelArray 或使用 ForkJoinPool

ParallelArray or using ForkJoinPool

我必须改进以下代码的性能,但我不知道要改进什么(或如何)。

目前,代码需要大约。 12 秒创建 94.478.400 个对象。

我想,我可以通过使用 ForkJoinPoolParallelArray 之类的东西来加快速度,但我不知道如何或在哪里将这种并行化放入我的代码中。

运行 主要方法:

import java.util.ArrayList;
import java.util.stream.Collectors;
import java.util.stream.DoubleStream;
import java.util.stream.IntStream;
import java.util.stream.Stream;

public class ParallelArray {

    // class to hold the permutations
    public static class Perm {

        public Perm(double a, double b, double c) {
            this.a = a;
            this.b = b;
            this.c = c;
        }

        double a;
        double b;
        double c;

        public String toString() {
            return "a=" + a + " b" + b + " c" + c;
        }
    }

    public static void main(String[] args) {

        var start = System.currentTimeMillis();
        var perms = new ArrayList<Perm>();

        // create first permutations
        var perm1 = computePerm(2).collect(Collectors.toList());

        // create second permutations
        var perm2 = computePerm(-2).collect(Collectors.toList());

        // now I permutate again with perm1 and perm2:
        // iterating is fast
        for (var p1 : perm1) {
            for (var p2 : perm2) {
                // but this operation needs too long
                // can I speed this up by using fork-join or parallelArray?
                perms.add(new Perm(p1.a + p2.a, p1.b + p2.b, p1.c + p2.c));
            }
        }

        var end = System.currentTimeMillis();

        // all the operations needs approx. 12 seconds
        System.out.println("diff: " + (end - start) + " millis");
        // it returns 94.478.400 objects
        System.out.println("objects: " + perms.size());
    }

    public static Stream<Perm> computePerm(int inc) {
        // I use a stream builder (to increase perfomance?)
        Stream.Builder<Perm> all = Stream.builder();
        // iterating is fast
        for (var a : arrayA().toArray()) {
            for (var b : arrayB().toArray()) {
                for (var c : arrayC().toArray()) {
                    // but this operation needs too long
                    // can I speed this up by using fork-join or parallelArray?
                    all.add(new Perm(a + inc, b + inc, c + inc));
                }
            }
        }

        return all.build();
    }

    public static IntStream arrayA() {
        return IntStream.rangeClosed(1, 30);
    }

    public static DoubleStream arrayB() {
        return DoubleStream.iterate(0, d -> d + 0.1).limit(18);
    }

    public static DoubleStream arrayC() {
        return DoubleStream.iterate(1, d -> d - 0.1).limit(18);
    }

}

编辑:

玩了一圈之后,我使用了 computePerm(int c) 的替代方法,它使用 parallelStream:

    public static Stream<Perm> computePermParallel(int inc) {
        Stream<Perm> all = arrayA().parallel().boxed().flatMap(a ->
        {
            return arrayB().parallel().boxed().flatMap(b ->
            {
                return arrayC().parallel().mapToObj(c ->
                {
                    return new Perm(a + inc, b + inc, c + inc);
                });
            });
        });
        return all;
    }

然而,当使用 computePermParallel(int inc) 而不是 computePerm(int inc) 时,性能更差:而不是大约。 12 秒,我需要等待大约。 19 秒。所以非并行版本比并行版本快得多,即使有如此大量的对象。不知道为什么。

有些事情对您不利..

  1. 9400 万个简单对象大约需要 2-4GB 的 RAM 用于该数据 - 您是否有足够的 RAM/为此配置......

  2. 持续重新分配/复制 ArrayList 支持 - 您可以使用 ArrayList 的 ensureCapacity 方法来帮助解决这个问题(只要 Array 运行s 超出容量,后备存储必须增长并复制数据 - 需要一些成本但没有收益)。

  3. 内存分配在 Java 中很慢 .. 如果您可以将数据表示为 3 个 double [not Double] 数组已经预先调整大小 - 您可以大大减少内存数量分配(3 个对象分配 vs 94+ 百万)——你只需要执行一些代码然后初始化数据..时间应该快得多(尽管你可能 运行 性能比你预期的还要差 -如果它导致 CPU 缓存被丢弃)。实际尝试预先创建 1 个 9400 万行数组,看看需要多长时间...

  4. 不必要的临时对象 - 在 arrayA() 的 for 循环中每次调用 arrayB() 和 arrayC() 都会导致构建一个新的临时结果,然后将其丢弃。但是在这个例子中你可以这样做 - 然后只有 3 个调用(和临时集),而不是 "nof B" * "nof C" * "nof A" + "nof B" * "nof A" + 。 .. [如果结果需要在每个循环上有所不同——那么请忽略这一点,但正如所写的那样,它读起来很好,但 运行 很糟糕。

所以改变这个 -

for (var a : arrayA().toArray()) {
for (var b : arrayB().toArray()) {
for (var c : arrayC().toArray()) { 

对此-

final int[] aArray = arrayA().toArray();
final double[] bArray = arrayB().toArray();
final double[] cArray = arrayC().toArray();
for (var a : aArray) {
for (var b : bArray) {
for (var c : cArray) { 

也许你会把它降低一个数量级 - 但我怀疑至少还有 1 到 2 秒 - 你正在使用大量内存 - 你真的需要吗(这听起来像是一个缺陷特殊情况)??

为了让您了解内存与 CPU 的相对成本,您可以测试创建所有对象,然后循环并设置所有值并计算差异的时间。

对您的代码的一些建议

  • 不断扩大 ArrayList 会导致大量副本和浪费时间。由于您已经预先知道大小,因此在初始化期间将其传递给您的 ArrayList。您可以使用 var perms = new ArrayList<Perm>(94478400); 对此进行测试。通过在数组列表 perms.ensureCapacity(94478400);

    上设置调用以下方法可以实现相同的目的
  • 在您的情况下,使用 Streams 并没有真正的优势,因为您创建了一个包含所有元素的流。然后将该流更改为列表。当您在收集之前应用某种过滤时,Stream 会有好处。在您的情况下,您只是从 Stream 重新分配到 List。您可以直接将其添加到列表中(使用正确的初始值参数)

  • ForkJoinPool: 运行 并行的事情在你的场景中并没有真正的帮助。如果您必须 运行 一些昂贵的计算,这将有所帮助。在您的情况下,您只需创建一个新的 Perm 对象并将其放入权限列表中。最大的问题是 ArrayList 不是线程安全的。像现在这样使用它会导致您丢失数据。解决方案是让 ForkJob return 一个临时列表,您在完成后将其合并到完整列表中。或者使用像 CopyOnWriteArrayList 这样的线程安全列表。这两种解决方案都会创建更多包含数据的列表,并导致数据被复制。因此,与其改善 运行 时间,很可能会使它变得更糟。