OpenGL扩展GL_ARB_shader_group_vote提供了一种机制,可以使用相同的值为用户定义的布尔条件对不同的着色器调用进行分组,这样该组内的所有调用只需要评估条件语句的一个相同的分支。例如:
if (anyInvocationARB(condition)) {
result = do_fast_path();
} else {
result = do_general_path();
}
因此,这里有潜在的性能提升,因为可以预先对调用进行分组,使得所有do_fast_path候选可以比其余的更快地执行。但是,我找不到任何有关此机制实际有用的信息,以及它是否有害。考虑一个带有dynamically uniform expression的着色器:
uniform int magicNumber;
void main() {
if (magicNumber == 1337) {
magicStuff();
} else {
return;
}
}
在这种情况下,用anyInvocationARB(magicNumber == 1337)
替换条件是否有意义?由于流是统一的,因此可能已经检测到在所有着色器调用中只需要评估两个分支中的一个。或者这是SIMD处理器不能以任何理由做出的假设?我在着色器中使用了大量基于统一值的分支,知道我是否真的可以从这个扩展中受益或者它是否甚至会降低性能会很有趣,因为我禁止统一的流优化。我自己还没有对此进行过分析,所以事先了解其他人的经历会很好,这可以给我带来一些麻烦。
不,没有意义。
再次阅读扩展的描述:
计算着色器在明确指定的线程组(本地工作组)上运行,但OpenGL 4.3的许多实现甚至会将非计算着色器调用分组并以SIMD方式执行。执行代码时
if (condition) { result = do_fast_path(); } else { result = do_general_path(); }
在调用之间存在分歧的情况下,SIMD实现可能首先调用do_fast_path()来调用其中为true的调用,并使其他调用处于休眠状态。一旦do_fast_path()返回,它可能会调用do_general_path()来调用其中为false并使其他调用处于休眠状态。在这种情况下,着色器执行快速路径和通用路径,并且仅使用所有调用的常规路径可能会更好。
所以现代GPU不一定会跳跃;他们可以改为执行if
表达式的两面,启用或禁用对条件通过或失败的任务的写入,除非所有任务都选择了分支的一侧。
这意味着两件事:
*Invocations
函数是没用的,因为它们在每个任务上评估为相同的值。allInvocationsARB
作为快速路径条件,因为其中一个任务可能需要通过一般路径。我对唯一的答案不满意,所以我要详细说明。
简单地单独添加“allInvocationsARB”不会提高性能(更新:是的,可以,请参阅答案的底部)。
正如OP所说,如果wavefront中没有任何线程为真,GPU将已经执行跳过。
那么allInvocationsARB如何帮助提高性能呢?
首先,您需要更改算法。我将使用一个例子。
假设您有64个项目可供使用。还有一个64x1x1线程的线程组(又名wavefront aka warp)。
原始计算着色器如下所示:
void main()
{
for( int i=0; i<64; ++i )
{
doExpensiveOperation( data[i], outResult[gl_GlobalInvocationID.x * 64u + i] );
}
}
也就是说,我们调用64个线程,每个线程迭代64次;从而产生4096个结果的输出。
但有一种快速的方法来检查我们是否应该跳过那种昂贵的操作。所以我们改进它:
void main()
{
for( int i=0; i<64; ++i )
{
if( needsToBeProccessed( data[i] ) )
doExpensiveOperation( data[i], outResult[gl_GlobalInvocationID.x * 64u + i] );
}
}
但问题出在这里:让我们说needToBeProccessed为所有64个工作项返回false。
整个波前将执行64次迭代并跳过昂贵的操作64次。
有更好的方法来解决这个问题。并且通过强制事先将每个线程强制处理单个项目:
bool cannotSkip = needsToBeProccessed( data[gl_LocalInvocationIndex], gl_LocalInvocationIndex );
在这里,我们使用gl_LocalInvocationIndex而不是i。这样,每个线程读取1个工作项。
现在,当我们使用此更改加上anyInvocationARB时,我们最终得到:
void main()
{
bool cannotSkip = needsToBeProccessed( data[gl_LocalInvocationIndex], gl_LocalInvocationIndex );
if( anyInvocationARB( cannotSkip ) )
{
for( int i=0; i<64; ++i )
{
if( needsToBeProccessed( data[i] ) )
doExpensiveOperation( data[i], outResult[gl_GlobalInvocationID.x * 64u + i] );
}
}
}
因为needsToBeProccessed为所有线程返回false,所以anyInvocationARB将返回false。
最后,着色器最终调用needsToBeProccessed()一次而不是64次。
这就是我们加快处理时间的方法。
这只有在我们或多或少确定大多数情况下,anyInvocationARB将返回false时才有效。
如果它总是返回true,那么我们最终会得到一个稍慢的计算着色器,因为现在需要ToBeProccessed将被调用65次(而不是64次),doExpensiveOperation将被调用64次。
更新:我意识到我在开始时犯了一个错误:只需在自己的CAN上添加“allInvocationsARB”即可提高性能。
这是因为没有它,您将执行动态分支。而当使用allInvocationsARB时,使用静态分支。有什么不同?
请考虑以下示例:
void main()
{
outResult[gl_LocalInvocationIndex] = 0;
if( gl_LocalInvocationIndex == 0 )
outResult[gl_LocalInvocationIndex] = 5;
}
这是一个动态分支。
GPU必须在发送结束时保证outResult [0] == 5以及所有其他元素outResult [i] == 0。
也就是说,GPU必须跟踪(也称为执行掩码)哪些线程在分支中是活动的而哪些不是。 wavefront中的非活动线程将执行指令,但是它们的结果将被屏蔽掉,就像它从未发生过一样。
现在让我们看看如果添加anyInvocationARB会发生什么:
void main()
{
outResult[gl_LocalInvocationIndex] = 0;
if( anyInvocationARB( gl_LocalInvocationIndex == 0 ) )
outResult[gl_LocalInvocationIndex] = 5;
}
现在这非常有趣,因为结果将是GPU特定的:
假设线程组大小为64x1x1。
现在:
但更重要的是,这是一个静态分支,因此GPU没有动态分支的开销,这需要跟踪非活动线程来掩盖结果。因此,简单地添加anyInvocationARB()可以提高性能,但请注意,如果您不小心,它也会以特定于GPU的方式影响结果。
有些情况下无关紧要,例如,如果您确定在所有值上运行代码将始终产生相同的结果。
例如:
void main()
{
outResult[gl_LocalInvocationIndex] = 5;
isDirty[gl_LocalInvocationIndex] = false;
if( gl_LocalInvocationIndex == 0 )
{
outResult[0] = 67;
isDirty[0] = true;
}
if( anyInvocationARB( isDirty[gl_LocalInvocationIndex] ) )
outResult[gl_LocalInvocationIndex] = 5;
}
在这种情况下,我们的代码和算法的性质保证在调度outResult [i] == 5后,无论是否存在anyInvocationARB。因此,AnyInvocationARB可用于通过使用静态分支而不是动态分支来提高性能。
当然,虽然简单地添加anyInvocationARB确实可以提高性能,但是进行大幅改进的最佳方法是以本答案前半部分描述的方式利用它。