GL_ARB_shader_group_vote如何影响着色器性能?

时间:2015-07-27 08:54:03

标签: opengl glsl

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处理器不能以任何理由做出的假设?我在着色器中使用了大量基于统一值的分支,知道我是否真的可以从这个扩展中受益或者它是否甚至会降低性能会很有趣,因为我禁止统一的流优化。我自己还没有对此进行过分析,所以事先了解其他人的经历会很好,这可以给我带来一些麻烦。

2 个答案:

答案 0 :(得分:1)

我对唯一的答案不满意,所以我会详细说明。

仅添加“ allInvocationsARB”不会提高性能(更新:是的,请参见答案底部)。

如OP所述,如果波前中的所有线程都不为真,则GPU将已经执行跳过。

那么allInvocationsARB如何帮助提高性能?

首先,您需要更改算法。我将使用一个示例。

让我们假设您有64个项目要工作。还有一个64x1x1线程的线程组(又名Wavefront或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] );
    }
}

但这是问题所在:假设对所有64个工作项,needsToBeProccessed返回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。

最后,着色器最终只调用一次而不是64次调用needsToBeProccessed()。

这就是我们加快处理时间的方式。

这仅在我们或多或少确定大多数情况下,anyInvocationARB将返回false时才有效。

如果它总是返回true,那么我们将以稍微慢一些的计算着色器结束,因为现在将requireToBeProccessed调用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。

  • AMD GCN使用64个线程的波前。
  • NVIDIA当前使用32个线程的波前(在NV术语中为“ warp”)。

现在:

  • 如果您在AMD上运行此代码,则outResult [i] == 5。
  • 如果您在NVIDIA上运行此代码,则第一个范围为[0; 32)将产生outResult [i] == 5;但第二个范围是[32; 64)将产生outResult [i] == 0。

但是更重要的是,这是一个静态分支,因此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确实可以提高性能,但是进行巨大改进的最佳方法是利用此答案上半部分所述的方法加以利用。

答案 1 :(得分:0)

不,没有意义。

再次阅读扩展程序的说明:

  

计算着色器在明确指定的线程组上运行(a   本地工作组),但OpenGL 4.3的许多实现甚至会分组   非计算着色器调用并以SIMD方式执行它们。什么时候   执行像

这样的代码
if (condition) {
  result = do_fast_path();
} else {
  result = do_general_path();
}
     

调用之间存在分歧,SIMD实现   可能首先调用do_fast_path()进行调用   是的,并让其他调用处于休眠状态。一旦do_fast_path()   返回,它可能调用do_general_path()进行调用   是假的,让其他调用处于休眠状态。在这种情况下,   着色器执行两者快速和一般路径,可能会更好   只使用所有调用的通用路径。

所以现代GPU不一定会跳跃;他们可能会执行if表达式的两面,启用或禁用对条件通过或失败的任务的写入,除非所有任务都选择了分支的一侧。

这意味着两件事:

  1. 在动态统一表达式上使用*Invocations函数是没用的,因为它们在每个任务上评估为相同的值。
  2. 您应该使用allInvocationsARB作为快速路径条件,因为其中一个任务可能需要通过一般路径。