重温名片光线跟踪器

2020-05-09 07:13:03

哎呦。现在,在这项任务中投入12个内核需要两倍的时间才能完成。我对结果感到惊讶,因为我小心翼翼地避免了错误共享,Scott Meyers[5]很好地描述了这个问题,当缓存一致性在运行时占据主导地位时就会出现这个问题。很明显,我没有抱住嘉年华。如果您返回到card_cores.cpp,您能发现错误吗?如果你猜对了libc';的rand(),那么你是对的。这个版本的随机生成器在调用之间维护内部状态,这意味着每个调用rand()的内核都会使包含该状态的所有其他内核缓存行变脏。解决方案是使用线程本地重新实现随机函数生成器。THREAD_LOCAL INT SEED=1;FLOAT R(){SEED=(214013*SEED+2531011);RETURN((SEED>;>;16)&;0xFFFF)/66635.0f;}。

$>;clang-O3-o multithard-lm-lpthread multithard.cpp$>;time./multithard>;/dev/null real 0m1.291suser 0m14.734ssys 0m0.109s。

此时,名片光线跟踪器运行时间从101秒缩短到1.3秒。我估计,在仅依靠CPU的情况下,它已接近我所能做到的最好水平。是时候换个更强壮的东西了。

在尝试了编译器优化和多线程之后,下一步是涉及到GPU。这是一个很好的时机,因为我刚刚为自己建造了一台新的PC[6]。15年来,我的GPU(Pascal GeForce GTX 1050Ti)首次仅比最新的图灵落后一代。进行GPGPU编程有两种选择:OpenCL和CUDA。我在id Software大学学习多年,这句格言“开放标准很好而且可移植”引发了我的思考,所以我考虑使用OpenCL。经过一些研究之后,它看起来很像GLSL,您需要创建缓冲区、绑定变量,并通过给出内核的源代码来创建Program。我对使用此方法处理编译器实现细节有过痛苦的记忆。使用Spir-V和clCreateProgramWithBinary有一些希望,但关于某些平台不支持它的报道很快就掩盖了这一点。最后,我选择了CUDA。事实证明,这是一种令人愉快的体验,程序员只需通过特殊限定符(如__global__和__device_)标记代码/数据位置,而NVCC编译器几乎负责所有事情。

有很多关于CUDA的书。他们的封面上似乎都有一个博格立方体。以Cuda为例,它是一个很好的开端,它将我从#34;HelloWorld;带到了一些编译和运行时不会崩溃的东西。通过最少的重构和主要依赖于限定符关键字,我想出了一个简单的card_cuda.cu。我原以为会有一场彻底的表演大屠杀,但事情比预期的要好。C:\Users\fab&>;NVCC-o card_cuda-O3 card_cuda.cuwarning:无法静态确定入口函数';_Z8GetColorPh';的堆栈大小C:\Users\fab&>;card_cuda>;nul时间:984ms。

该警告可能不是一件好事(稍后我会再谈到这一点),但程序运行成功。在零优化的情况下,GPU的性能比12核CPU高出30%。

为了进一步改进运行时,我发现CUDA C专业编程是一座金矿。这本书非常详细地解释了CUDA核心/流多处理器是如何工作的,以及如何评测/优化内核。这篇文章的其余部分植根于这本书,我强烈推荐它。

在过去的二十年里,NVIDIA GPU架构发生了巨大的变化[7]。幸运的是,编程模型保持不变。一个GPU需要数千个线程。这些指令被分组为32个包(称为WARP),其中所有这些指令都具有相同的指令指针(因此称为SIMT),并以锁步方式执行相同的指令。就性能而言,最重要的是让SM保持良好的指令、数据和偏差。NVIDIA提供了一个名为nvprof的性能分析工具,该工具允许测量诸如占用率和经度中的线程发散等指标。分支效率测量经纱中有多少纱线发散。我们的光线跟踪器的数字很好。我原本以为它的表现会很糟糕,因为光线到处都是反弹的,但它似乎是空间位置帮助线条连贯的。另一边的入住率糟糕透顶(最好是1.0)。似乎GPU的SMS不能在飞行中保持足够的曲速,这导致了饥饿。这与发出的警告NVCC相关。因为光线跟踪器使用递归,所以它使用了大量堆栈。实际上是如此之多,以至于SM无法让更多的人存活下来。问题在于代码的递归性质。这就是编译器早先无法猜测堆栈大小的原因。修改raytracer以使用循环而不是递归(card_cudb.cu)减少了堆栈的消耗量。这意味着占用时间是原来的两倍,运行时间减少了一半!C:\Users\fab>;nvcc-o card_cudb-O3 card_cudb.cuC:\Users\fab>;nvprof--已实现的指标_占用卡_