如何在不使用 if 语句(或三元)的情况下将负数设置为无穷大
How to set a negative number to infinity without using an if statement (or ternary)
我有如下一段代码:
for(uint i=0; i<6; i++)
coeffs[i] = coeffs[i] < 0 ? 1.f/0.f : coeffs[i];
它检查一个包含 6 个元素的数组,如果找到一个负数条目,则将其设置为无穷大,否则保持条目不变。
我需要在不使用任何 if 语句的情况下做同样的事情
预先声明:我还没有真正测试过这个,但我怀疑它真的比使用三元组更快。执行基准测试以查看它是否真的是优化!
此外:这些是 implemented/tested 在 C 中。它们应该很容易移植到 GLSL,但您可能需要显式类型转换,这可能会使它们(甚至)变慢。
有两种方法,具体取决于您是严格需要 INFINITY
还是只能使用较大的值。既不使用分支表达式也不使用语句,但它们确实涉及比较。两者都使用 C 中的比较运算符总是 return 或者 0
或 1
.
基于 INFINITY
的方法使用 2 元素数组,并让比较输出选择选择数组的元素:
float chooseCoefs[2] = {0.f, INFINITY}; /* initialize choice-array */
for(uint i = 0; i < 6; i++){
int neg = coefs[i] < 0; /* outputs 1 or 0 */
/* set 0-element of choice-array to regular value */
chooseCoefs[0] = coefs[i];
/* if neg == 0: pick coefs[i], else neg == 1: pick INFINITY */
coefs[i] = chooseCoefs[neg];
}
如果你可以使用一个正常(但很大)的值而不是 INFINITY
,你可以用两次乘法和一次加法代替:
#define BIGFLOAT 1000.f /* a swimming sasquatch... */
for(uint i = 0; i < 6; i++){
int neg = coefs[i] < 0;
/* if neg == 1: 1 * BIGFLOAT + 0 * coefs[i] == BIGFLOAT,
else neg == 0: 0 * BIGFLOAT + 1 * coefs[i] == coefs[i] */
coefs[i] = neg * BIGFLOAT + !neg * coefs[i];
}
同样,我没有对这些进行基准测试,但我的猜测是至少基于数组的解决方案比简单的三元组慢得多。不要低估编译器的优化能力!
一个明显的问题是 当输入小于 0 时你需要什么无穷大。
任意无穷大
如果结果可以是负无穷大,我会这样做:
coeffs[i] /= (coeffs[i] >= 0.0);
如果输入为正,coeffs[i] >= 0.0
产生 1.0,如果输入为负,0.0
产生。将输入除以 1.0 保持不变。将它除以 0 会产生无穷大。
正无穷大
如果它必须是正无穷大,您可以将其更改为:
coeffs[i] = (fabs(coeffs[i]) / (coeffs[i] >= 0.0);
通过在除法之前取绝对值,我们为负数产生的无穷大被强制为正数。否则,输入一开始是正的,所以 fabs
和除以 1.0 保持值不变。
性能
至于这是否真的会提高性能,这可能还有很多问题。现在,让我们看一下 CPU 的代码,因为 Godbolt 让我们可以很容易地检查它。
如果我们看这个:
#include <limits>
double f(double in) {
return in / (in >= 0.0);
}
double g(double in) {
return in > 0.0 ? in : std::numeric_limits<double>::infinity();
}
那么,让我们看看为第一个函数生成的代码:
xorpd xmm1, xmm1
cmplesd xmm1, xmm0
movsd xmm2, qword ptr [rip + .LCPI0_0] # xmm2 = mem[0],zero
andpd xmm2, xmm1
divsd xmm0, xmm2
ret
所以这还不算太糟糕——无分支,并且(取决于所涉及的确切处理器)在最合理的现代处理器上的吞吐量 大约 8-10 个周期。另一方面,这是为第二个函数生成的代码:
xorpd xmm1, xmm1
cmpltsd xmm1, xmm0
andpd xmm0, xmm1
movsd xmm2, qword ptr [rip + .LCPI1_0] # xmm2 = mem[0],zero
andnpd xmm1, xmm2
orpd xmm0, xmm1
ret
这也是无分支的——并且也没有那个(相对较慢的)divsd 指令。同样,性能会因特定处理器而异,但我们可能会计划吞吐量大约为 6 个周期——虽然不会 非常 比以前快,但可能至少比前一个快部分时间快了几个周期,而且几乎可以肯定永远不会慢。简而言之,它可能在几乎任何可能的情况下都更可取 CPU.
GPU 代码
GPU 有自己的指令集,当然——但考虑到它们因分支而受到的惩罚,它们的编译器(以及它们提供的指令集)可能至少与 CPU 确实如此,所以很可能直接的代码也可以很好地工作(尽管可以肯定地说,您需要检查它生成的代码或对其进行分析)。
我有如下一段代码:
for(uint i=0; i<6; i++)
coeffs[i] = coeffs[i] < 0 ? 1.f/0.f : coeffs[i];
它检查一个包含 6 个元素的数组,如果找到一个负数条目,则将其设置为无穷大,否则保持条目不变。
我需要在不使用任何 if 语句的情况下做同样的事情
预先声明:我还没有真正测试过这个,但我怀疑它真的比使用三元组更快。执行基准测试以查看它是否真的是优化!
此外:这些是 implemented/tested 在 C 中。它们应该很容易移植到 GLSL,但您可能需要显式类型转换,这可能会使它们(甚至)变慢。
有两种方法,具体取决于您是严格需要 INFINITY
还是只能使用较大的值。既不使用分支表达式也不使用语句,但它们确实涉及比较。两者都使用 C 中的比较运算符总是 return 或者 0
或 1
.
基于 INFINITY
的方法使用 2 元素数组,并让比较输出选择选择数组的元素:
float chooseCoefs[2] = {0.f, INFINITY}; /* initialize choice-array */
for(uint i = 0; i < 6; i++){
int neg = coefs[i] < 0; /* outputs 1 or 0 */
/* set 0-element of choice-array to regular value */
chooseCoefs[0] = coefs[i];
/* if neg == 0: pick coefs[i], else neg == 1: pick INFINITY */
coefs[i] = chooseCoefs[neg];
}
如果你可以使用一个正常(但很大)的值而不是 INFINITY
,你可以用两次乘法和一次加法代替:
#define BIGFLOAT 1000.f /* a swimming sasquatch... */
for(uint i = 0; i < 6; i++){
int neg = coefs[i] < 0;
/* if neg == 1: 1 * BIGFLOAT + 0 * coefs[i] == BIGFLOAT,
else neg == 0: 0 * BIGFLOAT + 1 * coefs[i] == coefs[i] */
coefs[i] = neg * BIGFLOAT + !neg * coefs[i];
}
同样,我没有对这些进行基准测试,但我的猜测是至少基于数组的解决方案比简单的三元组慢得多。不要低估编译器的优化能力!
一个明显的问题是 当输入小于 0 时你需要什么无穷大。
任意无穷大
如果结果可以是负无穷大,我会这样做:
coeffs[i] /= (coeffs[i] >= 0.0);
如果输入为正,coeffs[i] >= 0.0
产生 1.0,如果输入为负,0.0
产生。将输入除以 1.0 保持不变。将它除以 0 会产生无穷大。
正无穷大
如果它必须是正无穷大,您可以将其更改为:
coeffs[i] = (fabs(coeffs[i]) / (coeffs[i] >= 0.0);
通过在除法之前取绝对值,我们为负数产生的无穷大被强制为正数。否则,输入一开始是正的,所以 fabs
和除以 1.0 保持值不变。
性能
至于这是否真的会提高性能,这可能还有很多问题。现在,让我们看一下 CPU 的代码,因为 Godbolt 让我们可以很容易地检查它。
如果我们看这个:
#include <limits>
double f(double in) {
return in / (in >= 0.0);
}
double g(double in) {
return in > 0.0 ? in : std::numeric_limits<double>::infinity();
}
那么,让我们看看为第一个函数生成的代码:
xorpd xmm1, xmm1
cmplesd xmm1, xmm0
movsd xmm2, qword ptr [rip + .LCPI0_0] # xmm2 = mem[0],zero
andpd xmm2, xmm1
divsd xmm0, xmm2
ret
所以这还不算太糟糕——无分支,并且(取决于所涉及的确切处理器)在最合理的现代处理器上的吞吐量 大约 8-10 个周期。另一方面,这是为第二个函数生成的代码:
xorpd xmm1, xmm1
cmpltsd xmm1, xmm0
andpd xmm0, xmm1
movsd xmm2, qword ptr [rip + .LCPI1_0] # xmm2 = mem[0],zero
andnpd xmm1, xmm2
orpd xmm0, xmm1
ret
这也是无分支的——并且也没有那个(相对较慢的)divsd 指令。同样,性能会因特定处理器而异,但我们可能会计划吞吐量大约为 6 个周期——虽然不会 非常 比以前快,但可能至少比前一个快部分时间快了几个周期,而且几乎可以肯定永远不会慢。简而言之,它可能在几乎任何可能的情况下都更可取 CPU.
GPU 代码
GPU 有自己的指令集,当然——但考虑到它们因分支而受到的惩罚,它们的编译器(以及它们提供的指令集)可能至少与 CPU 确实如此,所以很可能直接的代码也可以很好地工作(尽管可以肯定地说,您需要检查它生成的代码或对其进行分析)。