从OpenCL 2.0规范,“6.3运算符”一章,第29页:
克。逻辑运算符和(&&),或(||)对所有标量和向量内置类型进行操作。对于 仅标量内置类型,和(&&)只会评估左手的右手操作数 操作数比较不等于0 。仅对于标量内置类型,或(||)将仅评估 右手操作数,如果左手操作数等于0 。对于内置矢量类型, 评估两个操作数,并按组件方式应用运算符。如果一个操作数是 标量和另一个是矢量,标量可能会受到通常的算术转换 到向量操作数使用的元素类型。然后将标量类型扩展为向量 与向量操作数具有相同数量的组件。操作完成 分量导致相同大小的矢量。
这意味着使用带有逻辑运算符的表达式将导致分支和线程分歧,从而导致某些并行平台上的性能损失。例如:
int min_nonzero(int a, int b)
{
return (a < b && a != 0)? a : b; // branch
}
这可以部分修复,如:
int min_nonzero(int a, int b)
{
return select(b, a, a < b && a != 0); // branch because of &&
}
可能使用算术来实现内置函数select
以避免分支(例如,作为线性插值)。但&&
中仍有分支。一种可能更好的方法:
int min_nonzero(int a, int b)
{
return select(b, a, (int)(a < b) & (int)(a != 0)); // branch free
}
但很快变得难以理解。
所以我的问题是:是否有更好的方法来说服OpenCL编译器放弃对布尔表达式的懒惰评估(不是全局但在特定情况下)?
以下是我在这件事上的实际实验,不再是一个问题了。在某些情况下仍然需要延迟评估,例如:
if(i < N && array[i] == x) // will go OOB without lazy evaluation
因此,优化程序不太可能完全禁用它,或者至少在所有适用的情况下都禁用它。
我正在查看由NVIDIA 320.49驱动程序生成的一些PTX,它只会优化右侧无数组访问的情况:
if(p[i] == n_end && i)
return;
编译成一个分支:
setp.ne.s32 %p2, %r17, %r5; // p[i] != n_end
setp.eq.s32 %p3, %r28, 0; // !i
or.pred %p4, %p2, %p3; // (p[i] != n_end || !i) = !(p[i] == n_end && i)
@%p4 bra BB2_3; // branch
ret;
BB2_3:
然而这:
int n_increment = 1;
for(++ i; i < n_cols_B && p[i + 1] == n_end; ++ i)
++ n_increment;
汇编为:
mov.u32 %r29, 1;
BB2_4:
mov.u32 %r6, %r28;
add.s32 %r8, %r6, 1;
ld.param.u32 %r24, [Fill_ColsTailFlags_v0_const_param_0];
setp.ge.u32 %p5, %r8, %r24;
@%p5 bra BB2_6; // branch if i < n_cols_B
shl.b32 %r19, %r6, 2;
ld.param.u32 %r25, [Fill_ColsTailFlags_v0_const_param_1];
add.s32 %r20, %r19, %r25;
ld.const.u32 %r21, [%r20+8];
setp.eq.s32 %p6, %r21, %r5;
@%p6 bra BB2_7; // branch if p[i + 1] == n_end
BB2_6:
shl.b32 %r22, %r5, 2;
ld.param.u32 %r27, [Fill_ColsTailFlags_v0_const_param_2];
add.s32 %r23, %r27, %r22;
st.global.u32 [%r23], %r29;
ret;
BB2_7:
add.s32 %r29, %r29, 1;
mov.u32 %r28, %r8;
bra.uni BB2_4;
似乎它对数组访问感到害羞,因为它不知道如何根据左边的条件分析数组访问的正确性。在这种情况下,条件的切换顺序为p[i + 1] == n_end && i < n_cols_B
摆脱了分支。将索引更改为常量i < n_cols_B && B_p[j] == n_end
(其中j = get_global_id(0)
在开始时初始化)不会删除分支。