并行化光线追踪器比单线程版本更慢的问题排查
光线追踪多线程并行化性能倒退问题排查
问题描述
基于《Ray Tracing in One Weekend》系列实现光线追踪器,采用图像切片分配给线程的方式并行渲染,但出现多线程版本性能不如单线程的情况:渲染400×400分辨率、1000 spp、10次光线反弹的图像时,单线程耗时64秒,16线程耗时82秒,2/4线程耗时也约64秒。核心代码如下:
void Camera::render(const Hittable& world, const Hittable& lights) { unsigned n_threads = std::thread::hardware_concurrency(); unsigned rows_per_job = img_height_val / n_threads; unsigned leftover = img_height_val % n_threads; std::vector<job_block_t> jobs; std::vector<std::thread> threads; for (unsigned i = 0; i < n_threads; ++i) { job_block_t job; job.row_start = i * rows_per_job; job.row_end = job.row_start + rows_per_job; if (i == (n_threads - 1)) { job.row_end += leftover; } job.row_size = img_width_val; jobs.push_back(job); } for (unsigned i = 0; i < (n_threads - 1); ++i) { threads.emplace_back([this, &world, &lights, &jobs, i] { color_per_job(world, lights, jobs[i]); }); } color_per_job(world, lights, jobs[n_threads - 1]); { std::unique_lock<std::mutex> lock(mutex); cv.wait(lock, [this, n_threads] { return image_blocks.size() == n_threads; }); } for (auto& t : threads) { if (t.joinable()) { t.join(); } } reconstruct_image(std::cout); std::clog << "\nDone.\n"; } void Camera::color_per_job(const Hittable& world, const Hittable& lights, job_block_t& job) { for (int j=job.row_start; j<job.row_end; ++j) { for (int i = 0; i < job.row_size; ++i) { Color pixel_color(0,0,0); for (int s_j = 0; s_j < samples_sqrt; ++s_j) { for (int s_i = 0; s_i < samples_sqrt; ++s_i) { Ray r = get_ray(i, j, s_i, s_j); pixel_color += ray_color(r, max_depth, world, lights); } } pixel_color *= pixel_samples_scale; pixel_color.set_x(linear_to_gamma(pixel_color.x())); pixel_color.set_y(linear_to_gamma(pixel_color.y())); pixel_color.set_z(linear_to_gamma(pixel_color.z())); const int index = j * job.row_size + i; job.indices.push_back(index); job.colors.push_back(pixel_color); } } { std::lock_guard<std::mutex> lock(mutex); image_blocks.push_back(job); cv.notify_one(); } } Color Camera::ray_color(const Ray& r, int depth, const Hittable& world, const Hittable& lights) const { if (depth <= 0) { // no light returned after too many bounces return Color(0, 0, 0); } HitRecord rec; double shadow_acne_offset = 0.001; // no hits if (!world.hit(r, Interval(shadow_acne_offset, infinity), rec)) { return background; } ScatterRecord srec; Color color_from_emission = rec.material()->emitted(r, rec, rec.u(), rec.v(), rec.point()); if (!rec.material()->scatter(r,rec,srec)) { return color_from_emission; } Color color_from_scatter = Color(); for (const auto& ray_t : srec.scattered_rays) { if (ray_t.skip_pdf || ray_t.pdf == nullptr) { color_from_scatter += ray_t.attenuation * ray_color(ray_t.skip_pdf_ray, depth-1, world, lights); } else { auto light_ptr = std::make_shared<HittablePDF>(lights, rec.point()); MixturePDF p(light_ptr, ray_t.pdf); Ray scattered = Ray(rec.point(), p.generate(), r.time()); double pdf_value = p.value(scattered.direction(), r.direction(), w); double scattering_pdf = rec.material()->scattering_pdf(r, rec, scattered, w); Color sample_color = ray_color(scattered, depth-1, world, lights); color_from_scatter += (ray_t.attenuation * scattering_pdf * sample_color) / pdf_value; } } return color_from_emission + color_from_scatter; } typedef struct JobBlock { int row_start; int row_end; int row_size; std::vector<int> indices; std::vector<Vec3> colors; } job_block_t; // block_job_t
核心问题分析
- 随机数生成器线程竞争:光线追踪依赖大量随机采样(如
get_ray中的采样、p.generate()的方向生成),若使用全局共享随机数对象,线程间会频繁竞争锁,直接导致性能暴跌,这是多线程光线追踪性能倒退的最常见原因。 - 冗余同步逻辑:主线程既用条件变量等待
image_blocks填满,又调用join等待线程结束。join本身会阻塞至线程完成,条件变量等待完全多余,增加了不必要的同步开销。 - 任务粒度太小:400行分给16线程,每个线程仅处理25行,线程创建、销毁的开销占比过高,抵消了并行收益。
- 超线程上下文切换开销:
std::thread::hardware_concurrency()返回逻辑核心数(如16核32线程),但光线追踪是CPU密集型任务,超线程会引发核心资源争抢,上下文切换频繁,反而降低效率。 - 数据拷贝开销:
color_per_job将job推入image_blocks时执行深拷贝(job包含两个std::vector),大内存拷贝占用额外时间。
修复建议
- 给每个线程分配独立随机数生成器:在
color_per_job内创建局部随机数实例(如std::mt19937 rng(std::random_device{}())),确保所有采样逻辑使用该局部生成器,禁止共享全局随机源。 - 移除冗余条件变量等待:删除主线程中
cv.wait的代码块,直接调用join等待所有线程完成即可。 - 使用移动语义减少拷贝:将
image_blocks.push_back(job)改为image_blocks.push_back(std::move(job)),利用移动语义避免std::vector的深拷贝。 - 调整线程数量:测试使用物理核心数的线程数(如逻辑核心数的一半),避免超线程带来的资源竞争。
- 优化任务粒度:增大每个线程处理的行数,比如每个线程处理至少64行,减少线程创建的相对开销。例如400行可设置6个线程,每个线程处理67行(最后一个线程68行)。
- 确认共享对象只读性:确保
world、lights及所有材质对象在渲染过程中完全只读,无任何线程修改的状态,避免缓存行颠簸(伪共享)。
内容的提问来源于stack exchange,提问作者Niccolò Tiezzi
相关产品推荐
相关产品推荐

