优化查找表的分支
Optimizing branching for lookup tables
WebGL 中的分支似乎类似于以下内容(转述自多篇文章):
着色器并行执行其代码,如果它需要在继续之前评估条件是否为真(例如使用 if
语句),那么它必须 diverge 并以某种方式与其他线程进行通信以得出结论。
也许这有点离谱 - 但最终,着色器中的分支问题似乎是每个线程可能会看到不同的数据。因此,仅使用制服进行分支通常是可以的,而动态数据上的分支则不行。
问题 1:这是正确的吗?
问题 2:这与相当可预测但不统一的事物(例如循环中的索引)有何关系?
具体来说,我有以下功能:
vec4 getMorph(int morphIndex) {
/* doesn't work - can't access morphs via dynamic index
vec4 morphs[8];
morphs[0] = a_Morph_0;
morphs[1] = a_Morph_1;
...
morphs[7] = a_Morph_7;
return morphs[morphIndex];
*/
//need to do this:
if(morphIndex == 0) {
return a_Morph_0;
} else if(morphIndex == 1) {
return a_Morph_1;
}
...
else if(morphIndex == 7) {
return a_Morph_7;
}
}
我这样称呼它:
for(int i = 0; i < 8; i++) {
pos += weight * getMorph(i);
normal += weight * getMorph(i);
...
}
从技术上讲,它工作正常 - 但我担心的是所有基于动态索引的 if/else 分支。在这种情况下,这会减慢速度吗?
为了比较,虽然在这里用几句简洁的词来解释是很棘手的 - 我有一个替代想法总是 运行 每个属性的所有计算。这可能涉及每个顶点 24 次多余的 vec4 += float * vec4
计算。通常,这会比索引上的 8 次分支更好还是更差?
注意:在我的实际代码中有更多级别的映射和间接,虽然它确实归结为相同的 getMorph(i)
问题,但我的用例涉及从两者获取索引循环中的索引,以及在统一整数数组中查找该索引
我知道这不是您问题的直接答案,但是...为什么不使用循环呢?
vec3 pos = weight[0] * a_Morph_0 +
weight[1] * a_Morph_1 +
weight[2] * a_Morph_2 ...
如果您想要通用代码(即您可以设置变形数量的地方),那么可以使用 #if
、#else
、#endif
发挥创意
const numMorphs = ?
const shaderSource = `
...
#define NUM_MORPHS ${numMorphs}
vec3 pos = weight[0] * a_Morph_0
#if NUM_MORPHS >= 1
+ weight[1] * a_Morph_1
#endif
#if NUM_MORPHS >= 2
+ weight[2] * a_Morph_2
#endif
;
...
`;
或使用字符串操作在 JavaScript 中生成着色器。
function createMorphShaderSource(numMorphs) {
const morphStrs = [];
for (i = 1; i < numMorphs; ++i) {
morphStrs.push(`+ weight[${i}] * a_Morph_${i}`);
}
return `
..shader code..
${morphStrs.join('\n')}
..shader code..
`;
}
通过字符串操作生成着色器是很正常的事情。您会发现所有主要的 3d 库都这样做(three.js、unreal、unity、pixi.js、playcanvas 等...)
至于分支是否慢,这确实取决于 GPU,但一般规则是,是的,无论如何完成都会慢一些。
您通常可以通过编写自定义着色器而不是尝试通用来避免分支。
而不是
uniform bool haveTexture;
if (haveTexture) {
...
} else {
...
}
只需编写 2 个着色器。一个有质感,一个没有。
另一种避免分支的方法是在数学上发挥创意。例如,假设我们想要支持顶点颜色或纹理
varying vec4 vertexColor;
uniform sampler2D textureColor;
...
vec4 tcolor = texture2D(textureColor, ...);
gl_FragColor = tcolor * vertexColor;
现在,当我们只想将顶点颜色设置为 textureColor
到 1x1 像素的白色纹理时。当我们只想要一个纹理时,关闭 vertexColor
的属性并将该属性设置为白色 gl.vertexAttrib4f(vertexColorAttributeLocation, 1, 1, 1, 1)
; 奖励!,我们可以通过提供纹理和顶点颜色来使用 vertexColors 调制纹理。
类似地,我们可以传入 0 或 1 以将某些事物乘以 0 或 1 以消除它们的影响。在您的变形示例中,以性能为目标的 3d 引擎将为不同数量的变形生成着色器。一个不关心性能的 3d 引擎会有 1 个支持 N 变形目标的着色器,只需将任何未使用的目标的权重设置为 0。
另一种避免分支的方法是 step
函数,它被定义为
step(edge, x) {
return x < edge ? 0.0 : 1.0;
}
所以你可以选择a
或b
和
v = mix(a, b, step(edge, x));
WebGL 中的分支似乎类似于以下内容(转述自多篇文章):
着色器并行执行其代码,如果它需要在继续之前评估条件是否为真(例如使用 if
语句),那么它必须 diverge 并以某种方式与其他线程进行通信以得出结论。
也许这有点离谱 - 但最终,着色器中的分支问题似乎是每个线程可能会看到不同的数据。因此,仅使用制服进行分支通常是可以的,而动态数据上的分支则不行。
问题 1:这是正确的吗?
问题 2:这与相当可预测但不统一的事物(例如循环中的索引)有何关系?
具体来说,我有以下功能:
vec4 getMorph(int morphIndex) {
/* doesn't work - can't access morphs via dynamic index
vec4 morphs[8];
morphs[0] = a_Morph_0;
morphs[1] = a_Morph_1;
...
morphs[7] = a_Morph_7;
return morphs[morphIndex];
*/
//need to do this:
if(morphIndex == 0) {
return a_Morph_0;
} else if(morphIndex == 1) {
return a_Morph_1;
}
...
else if(morphIndex == 7) {
return a_Morph_7;
}
}
我这样称呼它:
for(int i = 0; i < 8; i++) {
pos += weight * getMorph(i);
normal += weight * getMorph(i);
...
}
从技术上讲,它工作正常 - 但我担心的是所有基于动态索引的 if/else 分支。在这种情况下,这会减慢速度吗?
为了比较,虽然在这里用几句简洁的词来解释是很棘手的 - 我有一个替代想法总是 运行 每个属性的所有计算。这可能涉及每个顶点 24 次多余的 vec4 += float * vec4
计算。通常,这会比索引上的 8 次分支更好还是更差?
注意:在我的实际代码中有更多级别的映射和间接,虽然它确实归结为相同的 getMorph(i)
问题,但我的用例涉及从两者获取索引循环中的索引,以及在统一整数数组中查找该索引
我知道这不是您问题的直接答案,但是...为什么不使用循环呢?
vec3 pos = weight[0] * a_Morph_0 +
weight[1] * a_Morph_1 +
weight[2] * a_Morph_2 ...
如果您想要通用代码(即您可以设置变形数量的地方),那么可以使用 #if
、#else
、#endif
const numMorphs = ?
const shaderSource = `
...
#define NUM_MORPHS ${numMorphs}
vec3 pos = weight[0] * a_Morph_0
#if NUM_MORPHS >= 1
+ weight[1] * a_Morph_1
#endif
#if NUM_MORPHS >= 2
+ weight[2] * a_Morph_2
#endif
;
...
`;
或使用字符串操作在 JavaScript 中生成着色器。
function createMorphShaderSource(numMorphs) {
const morphStrs = [];
for (i = 1; i < numMorphs; ++i) {
morphStrs.push(`+ weight[${i}] * a_Morph_${i}`);
}
return `
..shader code..
${morphStrs.join('\n')}
..shader code..
`;
}
通过字符串操作生成着色器是很正常的事情。您会发现所有主要的 3d 库都这样做(three.js、unreal、unity、pixi.js、playcanvas 等...)
至于分支是否慢,这确实取决于 GPU,但一般规则是,是的,无论如何完成都会慢一些。
您通常可以通过编写自定义着色器而不是尝试通用来避免分支。
而不是
uniform bool haveTexture;
if (haveTexture) {
...
} else {
...
}
只需编写 2 个着色器。一个有质感,一个没有。
另一种避免分支的方法是在数学上发挥创意。例如,假设我们想要支持顶点颜色或纹理
varying vec4 vertexColor;
uniform sampler2D textureColor;
...
vec4 tcolor = texture2D(textureColor, ...);
gl_FragColor = tcolor * vertexColor;
现在,当我们只想将顶点颜色设置为 textureColor
到 1x1 像素的白色纹理时。当我们只想要一个纹理时,关闭 vertexColor
的属性并将该属性设置为白色 gl.vertexAttrib4f(vertexColorAttributeLocation, 1, 1, 1, 1)
; 奖励!,我们可以通过提供纹理和顶点颜色来使用 vertexColors 调制纹理。
类似地,我们可以传入 0 或 1 以将某些事物乘以 0 或 1 以消除它们的影响。在您的变形示例中,以性能为目标的 3d 引擎将为不同数量的变形生成着色器。一个不关心性能的 3d 引擎会有 1 个支持 N 变形目标的着色器,只需将任何未使用的目标的权重设置为 0。
另一种避免分支的方法是 step
函数,它被定义为
step(edge, x) {
return x < edge ? 0.0 : 1.0;
}
所以你可以选择a
或b
和
v = mix(a, b, step(edge, x));