作为一个个人项目,我一直在将《一个周末的光线追踪》指南翻译成 Java,以便更好地掌握 Java。显然,Java 比 C++ 慢,所以我尝试实现多线程来提高性能。但是,使用多线程时,代码的运行速度比使用单线程时慢。
我试图确保每个线程彼此独立(通过 VisualVM 测试),这样它们就不会等待。
我不完全确定如何处理内存采样的结果。
下面的代码是多线程渲染函数。本质上,我用样本/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));
}
}
最后,在单线程模式下,图像宽度为 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 线程一般都可以工作(废话)。以下是在您的应用程序中要查找的内容,以了解是什么搞砸了并行性:
world.createCopy(...)
和其他未并行运行但可能需要比您想象的时间更长的调用。最终图像生成有同样的问题。也许可以在其中一些调用周围添加一些带有时间戳的 println(...)
消息,以查看某个调用是否花费了比您预期更长的时间。getRay(...)
或其他共享方法同步公共数据?类似于缓存一致性问题。希望这里有帮助。