std::accumulate vs for 循环,光线追踪应用程序

std::accumulate vs for loop, raytracing application

此题基于this video on YouTube made with the purpose of reviewing this project

在视频中,主持人正在分析项目,发现以下代码块是导致性能问题的原因:

std::optional<HitRecord> HittableObjectList::Hit(const Ray &r, float t_min, float t_max) const {
    float closest_so_far = t_max;

    return std::accumulate(begin(objects), end(objects), std::optional<HitRecord>{},
        [&](const auto &temp_value, const auto &object) {
            if(auto temp_hit = object -> Hit(r, t_min, closest_so_far); temp_hit) {
                closest_so_far = temp_hit.value().t;
                return temp_hit;
            }

            return temp_value;
    });
}

我假设 std::accumulate 函数的功能类似于 for 循环。对那里的性能不满意(并且由于某种原因,探查器不会探查 lambda 代码[可能是一个限制?]),审阅者将代码更改为:

std::optional<HitRecord> HittableObjectList::Hit(const Ray &r, float t_min, float t_max) const {
    float closest_so_far = t_max;
    std::optional<HitRecord> record{};

    for(size_t i = 0; i < objects.size(); i++) {
        const std::shared_ptr<HittableObject> &object = objects[i];

        if(auto temp_hit = object -> Hit(r, t_min, closest_so_far); temp_hit) {
            closest_so_far = temp_hit.value().t;
            record = temp_hit;
        }
    }

    return record;
}

通过此更改,完成时间从 7 分 30 秒减少到 22 秒。

我的问题是:

审阅者确实提到了一些建议,例如由于调用量而避免在此处使用 std::optionals 和 std:shared_ptrs 以及改为在 GPU 上执行此代码,但现在我只对前面提到的那些点感兴趣。

声明:我没有运行高级测试,这只是我根据视频和代码的分析。

从我视频中的profiling来看,accumulate的热点在这里:

_Val = _Reduce_op(_Val, *_UFirst);

因为 _Reduce_op 只是我们的 lambda,并且分析显示这个 lambda 不是瓶颈,那么这意味着这里唯一昂贵的操作是复制赋值运算符 =.

正在查看HitRecord

struct HitRecord {
  point3 p;
  vec3 normal;
  std::shared_ptr<Material> mat_ptr;
  float t;
  bool front_face;
  ...

我们看到有一堆东西,包括 shared_ptr。如果 shared_ptr 不在此处,优化器可能会在不需要时删除副本。复制 shared_ptr 在热循环中 相关 昂贵,因为它涉及原子操作。

请注意,在分析的 accumulate 代码中,我们看到他们试图通过引入一个移动来在 c++20 中修复此问题。

#if _HAS_CXX20
        _Val = _Reduce_op(_STD move(_Val), *_UFirst);
#else // ^^^ _HAS_CXX20 ^^^ // vvv !_HAS_CXX20 vvv
        _Val = _Reduce_op(_Val, *_UFirst);
#endif // _HAS_CXX20

尽管要使此移动生效,编译器必须正确使用命名的 return 值优化,当函数中有多个 return 时,它并不总是这样做。您还必须更改 lambda 的签名,以便它采用值或右值而不是引用。从自动更改为命名类型无法解决问题。