You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

并行化光线追踪器比单线程版本更慢的问题排查

光线追踪多线程并行化性能倒退问题排查

问题描述

基于《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),大内存拷贝占用额外时间。

修复建议

  1. 给每个线程分配独立随机数生成器:在color_per_job内创建局部随机数实例(如std::mt19937 rng(std::random_device{}())),确保所有采样逻辑使用该局部生成器,禁止共享全局随机源。
  2. 移除冗余条件变量等待:删除主线程中cv.wait的代码块,直接调用join等待所有线程完成即可。
  3. 使用移动语义减少拷贝:将image_blocks.push_back(job)改为image_blocks.push_back(std::move(job)),利用移动语义避免std::vector的深拷贝。
  4. 调整线程数量:测试使用物理核心数的线程数(如逻辑核心数的一半),避免超线程带来的资源竞争。
  5. 优化任务粒度:增大每个线程处理的行数,比如每个线程处理至少64行,减少线程创建的相对开销。例如400行可设置6个线程,每个线程处理67行(最后一个线程68行)。
  6. 确认共享对象只读性:确保world、lights及所有材质对象在渲染过程中完全只读,无任何线程修改的状态,避免缓存行颠簸(伪共享)。

内容的提问来源于stack exchange,提问作者Niccolò Tiezzi

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.14 02:53:11