Java 多线程的性能比单线程差

问题描述 投票:0回答:1

作为一个个人项目,我一直在将《一个周末的光线追踪》指南翻译成 Java,以便更好地掌握 Java。显然,Java 比 C++ 慢,所以我尝试实现多线程来提高性能。但是,使用多线程时,代码的运行速度比使用单线程时慢。

我试图确保每个线程彼此独立(通过 VisualVM 测试),这样它们就不会等待。 Thread Timeline

我不完全确定如何处理内存采样的结果。

Memory Heap Histogram

Per Thread Allocation

VisualVM Overview

下面的代码是多线程渲染函数。本质上,我用样本/N 个样本渲染整个图像 N 次,其中 N 是线程数。然后,将图像平均为一幅最终图像。最后打印每个像素(这发生在线程之外,在主线程中)。如果您需要额外的上下文,我在 github 上有一个稍旧版本的代码(没有多线程)。 Java 光线追踪器

public void multiThreadedRender(final HittableList world, int threads) {
    initialize();
    samplesPerPixel /= threads;
    
    System.out.print("P3\n" + imageWidth + ' ' + imageHeight + "\n255\n");
    Vec3[] finalImage = new Vec3[imageWidth * imageHeight];
    for (int i = 0; i < finalImage.length; i++) {
        finalImage[i] = new Vec3();
    }

    ExecutorService executorService = Executors.newFixedThreadPool(threads);
    List<Future<Vec3[]>> futures = new ArrayList<>();

    for (int thread = 0; thread < threads; thread++) {
        HittableList worldCopy = world.createCopy();

        Callable<Vec3[]> renderTask = () -> {
            Vec3[] partialImage = new Vec3[imageWidth * imageHeight];

            for (int j = 0; j < imageHeight; j++) {
                //System.err.print("\rScanlines remaining: " + (imageHeight - j) + ' ');
                //System.err.flush();
                for (int i = 0; i < imageWidth; i++) {
                    Vec3 pixelColor = new Vec3(0,0,0);
                    Vec3 pixelCenter = pixel00Loc
                    .plus(
                        pixelDeltaU.multiply(i)
                    ).plus(
                        pixelDeltaV.multiply(j)  
                    );
                    for (int sample = 0; sample < samplesPerPixel; sample++) {
                        Ray r = getRay(i, j, pixelCenter);
                        pixelColor.plusEquals(rayColor(r, maxDepth, worldCopy));
                    }
                    partialImage[j*imageWidth+i] = pixelColor;
                }
            }
            System.err.print("\rDone.                           \n");
            return partialImage;
        };  
        futures.add(executorService.submit(renderTask));            
    }

    executorService.shutdown();

    try {
        executorService.awaitTermination(Long.MAX_VALUE, TimeUnit.NANOSECONDS);
    } catch (InterruptedException e) {
        // Handle interruption
        e.printStackTrace();
    }

     for (Future<Vec3[]> future : futures) {
        try {
            Vec3[] partialImage = future.get();
            for (int i = 0; i < finalImage.length; i++) {
                finalImage[i].plusEquals(partialImage[i]);
            }
        } catch (InterruptedException | ExecutionException e) {
            // Handle exceptions
            e.printStackTrace();
        }
    }

    for (int i = 0; i < finalImage.length; i++) {
        System.out.print(Color.getColor(finalImage[i], samplesPerPixel * threads));
    }
}

最后,在单线程模式下,图像宽度为 400 像素且每像素采样数为 128 时,总渲染时间约为 12 秒。对于多线程(以 4 个线程运行),大约需要 21 秒。在 16GB M1 Macbook Air 上运行。

为什么多线程代码的性能比单线程代码差?我该如何解决它?

但是,多重处理可以在创建四个进程时将渲染时间减少一半。

import java.io.File;
import java.io.IOException;

class Main {
    public static void main(String[] args) {
        long start = System.currentTimeMillis();
        for (int i = 0; i < 4; i++) {
            ProcessBuilder processBuilder = new ProcessBuilder("java", "-cp", "Documents/Code/java ray tracer/", "Main");
            try {
                processBuilder.redirectOutput(new File(String.format("image%d.ppm", i)));
                Process process = processBuilder.start();
                if (i == 3) {
                    try {
                        process.waitFor();
                    } catch (InterruptedException e) {
                        // TODO: handle exception
                    }
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        System.out.println("Total Time (ms): " + (System.currentTimeMillis() - start));
    }
}
java multithreading raytracing
1个回答
0
投票

最后,在单线程模式下,图像宽度为 400 像素且每像素采样数为 128 时,总渲染时间约为 12 秒。对于多线程(以 4 个线程运行),大约需要 21 秒。在 16GB M1 Macbook Air 上运行。

确实有问题,多线程情况花费了几乎两倍的时间。我刚刚尝试了一个缩减测试:

int numThreads = 1;
int numJobs = 4;
ExecutorService executorService = executors.newFixedThreadPool(numThreads);
for (int jobC = 0; jobC < numJobs; jobC++) {
    Callable<Void> renderTask = () -> {
        long total = 0;
        for (long i = 0; i < 100000000000L; i++) {
            total += i;
        }
        System.err.print("Done, total = " + total + "\n");
        return null;
    };
    executorService.submit(renderTask);
}
executorService.shutdown();
long start = System.currentTimeMillis();
executorService.awaitTermination(Long.MAX_VALUE, TimeUnit.NANOSECONDS);
System.out.println("Took " + ( System.currentTimeMillis() - start));

当 numThreads 设置为 1 时,程序花费了 130 秒,当 numThreads 设置为 4 时,程序花费了 36 秒。这是我的 MacBook Pro,运行 Chrome 等..

所以 Java 线程一般都可以工作(废话)。以下是在您的应用程序中要查找的内容,以了解是什么搞砸了并行性:

  • 首先,使用线程 = 1 运行相同的代码实际上运行得更快吗?尝试将作业数与线程数分开。串行运行 4 个作业中每一个作业的 1 个线程真的运行得更快吗?确保您正在比较苹果与苹果。
  • 虽然您似乎正在复制工作数据,但线程可能正在共享复杂的数据结构,因此您看到的是缓存一致性冲突,这会影响您的性能。我很惊讶这需要两倍的时间,但我想这是可能的。
  • 正如其他人提到的,请小心
    world.createCopy(...)
    和其他未并行运行但可能需要比您想象的时间更长的调用。最终图像生成有同样的问题。也许可以在其中一些调用周围添加一些带有时间戳的
    println(...)
    消息,以查看某个调用是否花费了比您预期更长的时间。
  • 是否有某种方法可以使
    getRay(...)
    或其他共享方法同步公共数据?类似于缓存一致性问题。

希望这里有帮助。

最新问题
© www.soinside.com 2019 - 2025. All rights reserved.