如何在不使用 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 或者 01.

基于 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 确实如此,所以很可能直接的代码也可以很好地工作(尽管可以肯定地说,您需要检查它生成的代码或对其进行分析)。