当然,要通过if
和switch
语句避免扭曲分歧,不惜一切代价来支持GPU。
但是,warp散度的开销是什么(仅调度执行某些行的线程的某些)与其他无用的算术相比?
考虑以下虚拟示例:
verison 1:
__device__ int get_D (int A, int B, int C)
{
//The value A is potentially different for every thread.
int D = 0;
if (A < 10)
D = A*6;
else if (A < 17)
D = A*6 + B*2;
else if (A < 26)
D = A*6 + B*2 + C;
else
D = A*6 + B*2 + C*3;
return D;
}
VS
版本2:
__device__ int get_D (int A, int B, int C)
{
//The value A is potentially different for every thread.
return A*6 + (A >= 10)*(B*2) + (A < 26)*C + (A >= 26)*(C*3);
}
我的真实场景更复杂(更多条件)但是同样的想法。
问题:
warp散度的开销(调度时间)是否太大,以至于版本1)比版本2慢?
版本2需要比版本1多得多的ALU,其中大部分都是在“乘以0”时浪费的(只有少数几个条件评估为1而不是0)。这是否会在无用操作中占用宝贵的ALU,延迟其他经线中的指令?
答案 0 :(得分:3)
这些问题的具体答案通常难以提供。影响两种情况比较分析的因素很多:
float
一样可能会对答案产生重大影响。幸运的是,有一些分析工具可以帮助提供清晰,具体的答案(或者可能表明这两种情况之间没有太大区别。)你已经做了很好的工作来识别你关心的两个特定案例。为什么不对2进行基准测试如果你想深入挖掘,分析工具可以提供有关指令重放的统计数据(由于经线发散引起的)带宽/计算限制指标等。
我必须对这个一揽子声明采取例外:
当然,在GPU上不惜一切代价避免使用if和switch语句进行扭曲分歧。
这根本不是真的。机器处理不同控制流的能力实际上是一个特征,它允许我们用更友好的语言(如C / C ++)对其进行编程,实际上它与其他一些不加速的技术不同。为程序员提供这种灵活性。
与任何其他优化工作一样,您应该首先将注意力集中在繁重的工作上。您提供的此代码是否构成了应用程序完成的大部分工作?在大多数情况下,将这种级别的分析工作放在基本上是胶水代码或不属于应用程序主要工作的部分是没有意义的。
如果这是您的代码的大部分工作,那么分析工具确实是获得有益的有意义答案的有效方式,这些答案可能比尝试进行学术分析更有用。
现在我的问题是:
warp散度的开销(调度时间)是否太大,以至于版本1)比版本2慢?
这取决于实际发生的具体分支水平。在最坏的情况下,通过32个线程的完全独立路径,机器将完全序列化,并且您实际上以峰值性能的1/32运行。线程的二元决策树类型细分不能产生这种最坏的情况,但肯定可以在树的末尾接近它。由于最终的完整线程分歧,可能会观察到此代码减速超过50%,可能减速80%或更高。但它在统计上取决于分歧实际发生的频率(即它与数据有关)。在最坏的情况下,我希望版本2更快。
版本2需要比版本1多得多的ALU,其中大部分都是在“乘以0”时浪费的(只有少数几个条件评估为1而不是0)。这是否会在无用操作中占用宝贵的ALU,延迟其他经线中的指令?
float
与int
可能实际上对此有所帮助,也许您可能会考虑尝试。但是第二种情况(对我而言)与第一种情况相比具有所有相同的比较,但是有一些额外的乘法。在浮动的情况下,机器可以每个时钟每个线程执行一次乘法,所以它非常快。在int情况下,它更慢,您可以看到具体的指令吞吐量取决于架构here。我不会过分担心这个级别的算术。而且,如果您的应用程序受内存带宽限制,它可能会使完全无差异。
另一种解决这一问题的方法是编写比较感兴趣代码的内核,编译为ptx(nvcc -ptx ...
)并比较ptx指令。这样可以更好地了解机器线程代码在每种情况下的样子,如果你只是执行类似指令计数的事情,你可能会发现这两种情况之间差别不大(在这种情况下应该支持选项2)