【问题标题】:How does GL_ARB_shader_group_vote influence shader performance?GL_ARB_shader_group_vote 如何影响着色器性能?
【发布时间】:2015-07-27 08:54:03
【问题描述】:

OpenGL 扩展 GL_ARB_shader_group_vote 提供了一种机制,可以将具有相同值的不同着色器调用分组为用户定义的布尔条件,这样该组内的所有调用只需要评估一个 - 相同 - 条件语句的分支.例如:

if (anyInvocationARB(condition)) {
    result = do_fast_path();
} else {
    result = do_general_path();
}

所以这里有一个潜在的性能提升,因为可以预先对调用进行分组,这样所有的 do_fast_path-candidates 可以比其他的执行得更快。但是,我找不到任何关于这种机制何时真正有用以及它是否有害的信息。考虑一个带有dynamically uniform expression 的着色器:

uniform int magicNumber;

void main() {
    if (magicNumber == 1337) {
        magicStuff();
    } else {
        return;
    }
}

在这种情况下,将条件替换为anyInvocationARB(magicNumber == 1337) 是否有意义?由于流程是统一的,因此可能已经检测到两个分支中只有一个需要在所有着色器调用中进行评估。或者这是 SIMD 处理器出于任何原因不得做出的假设?我在我的着色器中使用了很多基于统一值的分支,我很想知道我是否真的可以从这个扩展中受益,或者它是否会因为我抑制统一流优化而降低性能。我自己(还)没有对此进行分析,所以最好事先了解其他人的经历,这样可以省去一些麻烦。

【问题讨论】:

    标签: opengl glsl


    【解决方案1】:

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

    单独添加“allInvocationsARB”不会提高性能(更新:可以,请参阅答案底部)。

    正如 OP 所说,如果波前中没有线程为真,GPU 将已经执行跳过。

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

    首先你需要改变你的算法。我将使用一个例子。

    假设您有 64 个项目要处理。和一个 64x1x1 线程的线程组(又名波前又名扭曲)。

    原始计算着色器如下所示:

    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 个工作项返回 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,那么我们最终会得到一个稍微慢一点的计算着色器,因为现在需要调用 65 次(而不是 64 次),而 doExpensiveOperation 将被调用 64 次。

    更新:我意识到我一开始犯了一个错误:简单地添加“allInvocationsARB”可以提高性能。

    这是因为没有它,您将执行动态分支。而当使用 allInvocationsARB 时,将使用静态分支。有什么区别?

    考虑以下示例:

    void main()
    {
        outResult[gl_LocalInvocationIndex] = 0;
        if( gl_LocalInvocationIndex == 0 )
            outResult[gl_LocalInvocationIndex] = 5;
    }
    

    这是一个动态分支。

    GPU 必须在调度结束时保证 outResult[0] == 5 并且对于所有其他元素 outResult[i] == 0

    也就是说,GPU 必须跟踪(也称为执行掩码)哪些线程在分支中是活动的,哪些不是。波前中的非活动线程将执行指令,但它们的结果将被屏蔽掉,就好像它从未发生过一样。

    现在让我们看看如果我们添加 anyInvocationARB 会发生什么:

    void main()
    {
        outResult[gl_LocalInvocationIndex] = 0;
        if( anyInvocationARB( gl_LocalInvocationIndex == 0 ) )
            outResult[gl_LocalInvocationIndex] = 5;
    }
    

    现在这很有趣,因为结果将是特定于 GPU 的:

    假设线程组大小为 64x1x1。

    • AMD GCN 使用 64 个线程的波前。
    • NVIDIA 目前使用 32 个线程的波前(NV 术语中的“扭曲”)。

    现在:

    • 如果您在 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 确实可以提高性能,但巨大改进的最佳方法是按照本答案前半部分所述的方式利用它。

    【讨论】:

      【解决方案2】:

      不,没有意义。

      再次阅读扩展的描述:

      计算着色器在明确指定的线程组(a 本地工作组),但 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() 进行调用,其中 是错误的,并让其他调用处于休眠状态。在这种情况下, 着色器同时执行快速路径和一般路径,可能会更好 关闭所有调用的通用路径。

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

      这意味着两件事:

      1. 对动态统一表达式使用 *Invocations 函数是没有用的,因为它们在每个任务上的计算结果都相同。
      2. 您可能应该将allInvocationsARB 用于快速路径条件,因为其中一项任务可能需要通过一般路径。

      【讨论】:

      • “这意味着两件事” - 引用的描述并不意味着您的第一个含义。是否保证 SIMD 处理器永远不会评估统一条件表达式的两个分支?因为如果不是这样,在着色器代码明确允许之后,这个扩展现在是合法的。
      • @mOfl 从理论上讲,这并不能保证,但是您为 GLSL 实现提供了这样做的机会。实际上,如果它没有带来好处,那么实施这个扩展就没有意义。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2021-09-17
      • 2018-06-10
      相关资源
      最近更新 更多