在GPU避免分支的方法
Authored by Brandon Fogerty(XR Graphics Engineer at Unity Technologies.) with Additional Organized by JP.Lee(李正彪)[email protected]
概要
这边来稿文章中,希望了解如何编写GPU着色器友好型代码,以避免和分支相关的性能费用。
"分支"有着什么意义?
使用明确的 if / then随时都可以产生分支。编译器遇到条件会作出命令。GPU可去的地方有两处。
因此需要决定要使用哪种代码路径。以下示例展示了在汇编GPU里面运行的示例。
运算变量设置为幻数7,则添加该数字。否则的话要减掉。
为什么要避免分支?
为了增加CPU使用率,大多数计算机都尝试在管线上执行尽可能多的任务。汇编指令按顺序执行,CPU会尝试在尽可能多的CPU内核上执行尽可能多的指令。举个简单的例子。
想象一下,在Twilight Zone内,我们就是世界上最高效的程序员!
用两行代码写了电子游戏!
CPU会尽可能在每一个CPU内核上各执行一个,共计两个光荣的指令。
这是计算机能够更有效的运行代码。
但是在分支时,计算机可能会花时间准备未运行的代码结果。从结论上看处理器时间被浪费,影响了游戏或应用程序的应答能力。
CPU 侧分枝误诊通常会导致周期遗漏超过40次。– Charles Sanglimsuwan (集成开发人员相关工程师)
运气不错,最新的CPU处理器速度惊人,实际分支预测出色,因此分支错失几乎不会有问题。
但是GPU仍然存在性能问题。
GPU尝试并行解决大量计算,因此大部分GPU不支持分支预测
GPU为什么会发生性能问题?
GPU为生成美丽的图像,喜欢并行进行很多工作!
GPU为进行多种固有的结果计算,进行了精心设计,解决了通过单一滤镜(例如:着色器程序)实施的多种输入和相关的问题。
这就是渲染经常使用GPU(Graphics processor units)的原因。通过几个Shader程序运行位置,法线,贴图坐标等具有不同属性的固有顶点,该Shader程序输出在画面上表现的大量固有像素颜色。
举个例子,显示器是1080 × 960这样的一般HD像素的情况下,GPU在一组输入时,将计算1080 × 960 = 1,036,800个的固有像素颜色值!
那是很多的计算结果!
想象一下在1080x960的高清游戏体验。游戏尝试每秒渲染60个固有的图像或帧。因此GPU在1秒之内需要计算1080x960x60以上的固有像素颜色值!
结论上看一秒之内可以计算62,208,000个固有像素值!
撰写本文的节点,NVDIA最现金的GPU是GeForce RTX 2080 Ti。它有着比英特尔最先进的CPU处理器i9-7980XE更多4,352个Cuda内核,18个内核。
非常大的差异吧?
GPU有大量的处理内核,但是与最新的CPU相比,其核心处理数量仍不足百万。
至少目前还没有!显卡在物理上太大了,可能会很昂贵。
因此,GPU将尝试对要解决的问题类型进行特定假设。GPU充分利用SIMD。SIMD表示单个命令的多种数据。SIMD允许以并行方式为多个输入运行计算。SIMD通常希望输入和输出位于相邻的内存块中。因此SIMD操作不必加载每个输入并单独保存每个结果,而是加载输入并将结果保存为单个加载/保存操作,从而减少昂贵的内存加载和保存。SIMD的使用要求内存布局严格,并在应用程序设计中维持更好地内存缓存一致性上有积极作用。SIMD在CPU和GPU均可使用.
在CPU使用SIMD的示例可参考此处。与CPU程序不同,GPU程序几乎始终使用SIMD。为了充分利用SIMD,GPU通常每个内核都有很多ALU。ALU是算术逻辑设备的缩写。ALU执行数学命令。举个例子,使用“add 4, 3”命令计算结果7。因此,单一内核上有8个ALU的话,就可以同事并行运行8个计算。即,单一GPU内核有8个ALU,则单个GPU内核则可以同事计算8个像素值!但是,着色器阶段使用动态分支会发生什么呢?核心是必须执行所有代码路径,并最终丢弃不满足条件的代码路径。这意味着ALU会浪费时间执行任何未使用的操作。也就是说渲染图像需要更长的时间。
使用示例:
在Unity里,使用立体shader以保证双眼能够准确渲染。左眼以绿色表示渲染,右眼渲染的显示为红色。下面是使用立体shader渲染双眼锁渲染的单个球体。
针对此的纯接近方式是基于unity_StereoEyeIndex变数的条件方法。
Shader "XR/StereoEyeIndexColor"
{
Properties
{
_LeftEyeColor("Left Eye Color", COLOR) = (0,1,0,1)
_RightEyeColor("Right Eye Color", COLOR) = (1,0,0,1)
}
SubShader
{
Tags {
"RenderType" = "Opaque" }
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
float4 _LeftEyeColor;
float4 _RightEyeColor;
#include "UnityCG.cginc"
struct appdata
{
float4 vertex : POSITION;
UNITY_VERTEX_INPUT_INSTANCE_ID
};
struct v2f
{
float4 vertex : SV_POSITION;
UNITY_VERTEX_OUTPUT_STEREO
};
v2f vert (appdata v)
{
v2f o;
UNITY_SETUP_INSTANCE_ID(v);
UNITY_INITIALIZE_OUTPUT(v2f, o);
UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(o);
o.vertex = UnityObjectToClipPos(v.vertex);
return o;
}
fixed4 frag (v2f i) : SV_Target
{
UNITY_SETUP_STEREO_EYE_INDEX_POST_VERTEX(i);
if(unity_StereoEyeIndex ==