我一直在关注教程系列“一个周末的光线追踪”,这在学习光线追踪方面似乎相对规范。
我一直在尝试使用 OpenMP 来加速代码,但我收到了一些相当令人失望的结果,并且从这个 Github 讨论我相信其他人已经实现了更好的加速。
以下是我迄今为止看到的一些用户的报告:
还值得注意的是,这些更改是非常局部的:写入缓冲区而不是 std::cout 行;将缓冲区写入文件的新方法;上面的单个 OpenMP 行 for 循环输出图像的行。
通过在多样本循环之前使用#pragma omp parallel for,我能够实现显着的加速,这一点在第二本书的最后场景中变得非常明显
使用 OpenMP,您可以通过两行 OpenMP 注释来实现此目的,而不需要对代码本身进行任何更改
使用这些策略,我无法获得任何显着的加速。
我复制了最新版本的 Github 存储库(v4.0.1)并致力于“一个周末”部分。
我在示例 for 循环周围添加了注释
#pragma omp parallel for reduction(+:pixel_color)
(具有标题 for (int sample = 0; sample < samples_per_pixel; sample++)
),以及标题 #pragma omp declare reduction(+ : vec3 : omp_out+=omp_in) initializer(omp_priv(0,0,0))
来定义缩减。
我只使用
cam.render(world)
来计时完成 std::chrono::steady_clock
所需的时间。渲染默认场景时,速度仅提高了 1.74 倍。然而,考虑到我使用的是 8 核(并使用 omp_get_num_procs()
验证了该数字),这感觉太低了。
我恢复到commit 2e5cc2e(除了它于 2020 年 12 月 9 日发布这一事实之外没有其他原因,同一天另一位 Github 用户发表了一篇关于使用 OpenMP 实现显着加速的帖子)。我修改了代码以写入 2D 矢量
image
而不是输出到 stdout,然后将 image
写入文件。我计算了填充颜色所需的时间 image
。 scene.h
中修改后的代码如下所示:
std::vector<std::vector<color>> image(image_height, std::vector<color>(image_width));
omp_set_num_threads(8);
#pragma omp parallel for
for (int j = image_height-1; j >= 0; --j) {
for (int i = 0; i < image_width; ++i) {
color pixel_color(0,0,0);
for (int s = 0; s < samples_per_pixel; ++s) {
auto u = (i + random_double()) / (image_width-1);
auto v = (j + random_double()) / (image_height-1);
ray r = cam.get_ray(u, v);
pixel_color += ray_color(r, max_depth);
}
image[j][i] = pixel_color * pixel_samples_scale;
}
}
这仅给我带来了 2.34 倍的加速,但同样,我使用的是 8 核,并且期望更高。
我一直在使用这些 C++ 标志进行编译:
-Wall -std=c++17 -m64 -I. -fopenmp
。所有头文件都受 #ifndef
保护,因此无需在顶部使用 #pragma once
。我还尝试过使用 schedule(dynamic)
(这对于光线追踪来说似乎是合理的),但这只会降低加速比。
有关此问题的更多信息可以再次在最近创建的这个 Github 讨论中看到。我相信更快的加速应该很容易实现,我只是不确定为什么我的标题没有提供这一点。
感谢您的任何意见,如果我可以提供更多详细信息,请告诉我。
一种可能性是在代码内循环的每次迭代中使用两次对
random_double
的调用。
书籍提供了 random_double 的两种单独的实现:
inline double random_double() {
static std::uniform_real_distribution<double> distribution(0.0, 1.0);
static std::mt19937 generator;
return distribution(generator);
}
和:
inline double random_double() {
// Returns a random real in [0,1).
return std::rand() / (RAND_MAX + 1.0);
}
如果您使用的是
std::rand()
的包装版本,则缩放问题是可以理解的。问题的根源相当简单:std::rand
通常有一个种子来维持一次调用和下一次调用之间的状态。在每次调用期间,该状态都会更新。有几种不同的方法可以做到这一点,同时防止种子被损坏。一种是使用互斥锁,因此对 std::rand
的调用(大部分)是序列化的。另一种方法是(在幕后)创建一个线程本地种子值,这样每个线程都有自己的视图可以使用,并且每个线程都可以更新种子而不影响其他线程。这本身就带来了一些困难,但是当您添加更多线程时,它的扩展性确实会更好。
不过 std::mt19937
是一个 C++ 对象。该对象包含生成器的种子。该类型的每个对象都有自己的种子。因此,尽管您可能需要做一点额外的工作来确保每个线程都有自己的随机数生成器对象,但当/如果您这样做时,它几乎可以确保您不会在线程之间共享状态来限制当您使用更多线程执行时进行扩展。