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::accumulate
会在这里给予这么大的惩罚?
- 如果不使用
auto
s,使用显式类型,性能会更好吗?
审阅者确实提到了一些建议,例如由于调用量而避免在此处使用 std::optional
s 和 std:shared_ptr
s 以及改为在 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 的签名,以便它采用值或右值而不是引用。从自动更改为命名类型无法解决问题。
此题基于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::accumulate
会在这里给予这么大的惩罚? - 如果不使用
auto
s,使用显式类型,性能会更好吗?
审阅者确实提到了一些建议,例如由于调用量而避免在此处使用 std::optional
s 和 std:shared_ptr
s 以及改为在 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 的签名,以便它采用值或右值而不是引用。从自动更改为命名类型无法解决问题。