今天,我们将看看另外两个地方,pbrt花了大量时间来解析迪斯尼动画片
“ Moana”中的场景。 让我们看看这里是否有可能提高生产力。 这以pbrt-v3中的谨慎操作结束。 在另一篇文章中,我将探讨如果我们放弃对变更的禁令,我们可以走多远。 在这种情况下,源代码将与《
基于物理的渲染 》一书中描述的系统相差太大。
解析器优化
在
上一篇文章中介绍了性能改进之后,在pbrt解析器中花费的时间比例从一开始就非常重要,自然而然增加了更多。 当前,启动时的解析器大部分时间都花在了上面。
我终于
集思广益,并为pbrt场景
实现了手动编写的标记器和解析器 。
pbrt场景文件的格式很容易
解析 :如果不考虑引号行,令牌用空格分隔,语法非常简单(永远不需要期待多个令牌),但是您自己的解析器仍然是您需要的一千行代码编写和调试。 它可以帮助我在许多场景上进行测试。 修复明显的小故障之后,我继续工作,直到设法渲染出与以前完全相同的图像:由于替换了解析器,因此像素之间应该没有任何差异。 在此阶段,我绝对可以确保一切都正确完成。
我试图使新版本尽可能高效,使输入文件尽可能地受到
mmap()
并使用C ++ 17中的
std::string_view
的新实现来最大程度地减少从文件内容中创建字符串的副本。 另外,由于
strtod()
在以前的跟踪中花费了大量时间,因此我特别小心地编写
parseNumber()
:单位整数和正整数分别处理,并且在标准情况下,将pbrt编译为使用32位浮点数,使用
strtof()
代替
strtod()
1 。
在创建新解析器的实现的过程中,我有点担心旧的解析器会更快:最后,flex和bison已经开发和优化了很多年。 我无法事先弄清楚编写新版本是否会浪费所有时间,直到我完成并使其正常工作。
令我高兴的是,我们自己的解析器取得了巨大的胜利:flex和bison的泛化使性能大大降低,以至于新版本轻松地超过了它们。 多亏了新的解析器,启动时间减少到13分钟21 s,也就是说,它又加速了1.5倍! 另外一个好处是,现在可以从pbrt构建系统中删除所有的flex和bison支持。 一直令人头疼,尤其是在Windows下,大多数人默认情况下未安装它。
图形状态管理
在极大地加快了解析器的速度之后,出现了一个新的烦人的细节:在此阶段,大约10%的设置时间花费在函数
pbrtAttributeBegin()
和
pbrtAttributeEnd()
,并且大部分时间用于分配并释放了动态内存。 在第一次运行(耗时35分钟)期间,这些功能仅花费了大约3%的执行时间,因此可以忽略。 但是,通过优化始终是这样的:当您开始摆脱大问题时,小问题变得更加重要。
pbrt场景描述基于图形的分层状态,该状态指示当前的变换,当前的材质等。 在其中,可以制作当前状态的快照(
pbrtAttributeBegin()
),在将新几何图形添加到场景之前对其进行更改,然后返回到原始状态(
pbrtAttributeEnd()
)。
图形状态存储在具有意外名称...
GraphicsState
的结构中。 要将
GraphicsState
对象的副本存储在已保存的图形状态堆栈中,请使用
std::vector
。 查看
GraphicsState
成员,我们可以假设问题的根源-从名称到纹理和材质实例的三个
std::map
:
struct GraphicsState {
检查这些场景文件,我发现大多数保存和恢复图形状态的情况都在以下行中执行:
AttributeBegin ConcatTransform [0.981262 0.133695 -0.138749 0.000000 -0.067901 0.913846 0.400343 0.000000 0.180319 -0.383420 0.905800 0.000000 11.095301 18.852249 9.481399 1.000000] ObjectInstance "archivebaycedar0001_mod" AttributeEnd
换句话说,它更新当前转换并实例化该对象。 不会更改这些
std::map
的内容。 创建它们的完整副本-分配红黑树节点,增加公共指针的引用计数,分配空间和复制字符串-几乎总是在浪费时间。 恢复图形的先前状态时,将释放所有这些内容。
我用指针
std::shared_ptr
替换了每个映射,以映射并实现了写时复制方法,其中仅在需要更改其内容时才在属性的begin / end块内进行复制。
更改并不是特别困难,但是将启动时间减少了超过一分钟,这使我们在渲染开始之前的20 s处理时间为12分钟-再次是1.08倍的加速。
渲染时间呢?
细心的读者会注意到,到目前为止,我还没有谈到渲染时间。 令我惊讶的是,即使开箱即用,它也可以忍受:pbrt可以在十二个处理器内核上以每个像素数百个样本的速度渲染电影品质的场景图像,持续两到三个小时。 例如,此图像(最慢的图像之一)在2小时51分36秒内呈现:
pbrt-v3渲染的Moana沙丘,分辨率为2048x858,每个像素256个样本。 在具有12个内核/ 24个线程(频率为2 GHz)和最新版本的pbrt-v3的Google Compute Engine实例上,总渲染时间为2小时51分36秒。我认为,这似乎是一个令人惊讶的合理指标。 我确信仍有改进的余地,对大部分时间花费的地方进行仔细的研究会发现很多“有趣”的东西,但是到目前为止,没有特殊的理由。
进行剖析时,发现大约60%的渲染时间花费在光线与对象的交点上(大多数操作绕过BVH执行),而25%的时间用于搜索ptex纹理。 这些比率类似于简单场景的指标,因此乍看之下这里显然没有问题。 (但是,我确信Embree可以在更少的时间内跟踪这些光线。)
不幸的是,并行可伸缩性不是很好。 我通常会看到1400%的CPU资源用于渲染,而理想的情况是2400%(在Google Compute Engine中的24个虚拟CPU上)。 看来问题与ptex锁定期间的冲突有关,但我尚未对此进行更详细的研究。 pbrt-v3很可能不为光线跟踪器中的间接光线计算光线差; 反过来,此类光束始终可以访问最详细的MIP纹理级别,这对于纹理缓存不是很有用。
结论(对于pbrt-v3)
纠正了图形状态的管理后,我遇到了一个极限,在此之后,无需对系统进行重大更改就可以取得进一步的进步; 所有其他时间都花了很多时间,与优化没有任何关系。 因此,至少在pbrt-v3上,我将对此进行详细介绍。
总的来说,进度是很严重的:渲染之前的发射时间从35分钟减少到12分钟20秒,即总加速为2.83倍。 此外,由于转换缓存的巧妙使用,内存使用量已从80 GB减少到69 GB。 如果您正在与最新版本的pbrt-v3同步(或者如果您在过去的几个月中已完成此操作),则现在可以使用所有这些更改。并且我们开始了解该场景的
Primitive
内存是多么的垃圾。 我们想出了如何节省另外18 GB的内存,但是没有在pbrt-v3中实现它。
在我们进行所有优化之后,这12分20秒的时间如下:
功能/操作 | 运行时间百分比 |
---|
建立BVH | 34% |
解析( strtof() 除外) | 21% |
strtof() | 20% |
转换缓存 | 7% |
读取PLY文件 | 6% |
动态内存分配 | 5% |
转换倒置 | 2% |
图形状态管理 | 2% |
其他 | 3% |
将来,提高性能的最佳选择将是在启动阶段使用更大的多线程:场景解析过程中几乎所有内容都是单线程的。 我们最自然的首要目标是建立BVH。 分析诸如读取PLY文件,为对象的单个实例生成BVH并在后台异步执行它们之类的事情也将很有趣,而解析将在主线程中执行。
在某个时候,我将看看
strtof()
实现是否更快。 pbrt仅使用系统提供的内容。 但是,在选择未经充分测试的替换项时应格外小心:解析浮点值是程序员必须完全确定的方面之一。
进一步减轻解析器的负担看起来也很有吸引力:我们仍然有17 GB的文本输入文件用于解析。 我们可以为pbrt输入文件添加二进制编码支持(可能类似于
RenderMan方法 ),但是我对此想法有不同的感觉。 在文本编辑器中打开和修改场景描述文件的功能非常有用,而且我担心有时二进制编码会使学生在学习过程中使用pbrt感到困惑。 这是正确的pbrt解决方案可能与商业化生产级别解决方案不同的情况之一。
跟踪所有这些优化并更好地了解各种解决方案非常有趣。 事实证明,pbrt具有意想不到的假设,这些假设会干扰这种级别的复杂性。 所有这些都是一个很好的例子,说明了使渲染研究人员能够访问具有高度复杂性的真实制作场景对于它的重要性。 我再次要感谢迪士尼,因为我花了宝贵的时间来处理这个场景并将其发布到公共领域。
在下一篇文章中 ,我们将研究如果允许pbrt进行更彻底的更改可以进一步提高性能的方面。
注意事项
- 在我测试过的Linux系统上,
strtof()
并不比strtod()
快。 值得注意的是,在OS X上, strtod()
大约快两倍,这完全是不合逻辑的。 出于实际原因,我继续使用strtof()
。