是否有一种无分支的方法可以将数字移动到另一个数字而不超过它?

Vit*_*meo 3 c++ math floating-point optimization branchless

我想实现一个符合以下接口和契约的函数:

void move_towards(float& value, float target, float step) 
    // Moves `value` towards `target` by `step`. 
    // `value` will never go beyond `target`, but can match it.
    // If `step == 0.0f`, `value` is unchanged.
    // If `step > 0.0f`, `std::abs(target - value)` decreases.
    // If `step < 0.0f`, the behavior is undefined.
Run Code Online (Sandbox Code Playgroud)

其想法是使用此函数逐渐将现有浮点值移向另一个浮点值,而不会超过目标。例如,这对于在值之间执行线性转换作为游戏循环执行的一部分非常有用。

这是一个测试用例示例:

float value = 5.0f;
move_towards(value,  10.f,  1.f); assert(value ==  6.0f);
move_towards(value,  10.f,  1.f); assert(value ==  7.0f);
move_towards(value, -5.f,   5.f); assert(value ==  2.0f);
move_towards(value, -5.f,   5.f); assert(value == -3.0f);
move_towards(value, -5.f,   5.f); assert(value == -5.0f);
move_towards(value, -5.f,   5.f); assert(value == -5.0f);
move_towards(value,  0.f,  15.f); assert(value ==  0.0f);
Run Code Online (Sandbox Code Playgroud)

std::copysign我使用和的组合尝试了一些无分支的想法std::clamp,但它们在某些边缘情况下总是失败。最后,我求助于使用分支版本:

void move_towards(float& value, float target, float step) 
{
    if (value < target)
    {
        value += step;
        if (value > target)
        {
            value = target;
        }
    }
    else if (value > target)
    {
        value -= step;
        if (value < target)
        {
            value = target;
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

住在 godbolt.org 上

  • 是否可以实现move_towards以产生无分支指令?
  • 如果不是,是否可以至少减少分支数量?
  • 无论如何,哪个版本提供最佳的运行时性能?

Eri*_*hil 5

所需的结果是value - stepvalue + step、 和的中位数target,因此有效:

\n
void move_towards(float &value, float target, float step) \n{\n    value = std::max(value - step, std::min(value + step, target));\n}\n
Run Code Online (Sandbox Code Playgroud)\n

target要了解这一点,请考虑位于 和 之间、之间或之上value - step的情况value + step

\n
    \n
  • 如果target\xe2\x89\xa4 value - step< value + step,那么value可以完全step向下趋向target,所以我们想要value - step
  • \n
  • 如果value - step< target< value + step,则value不能完全step朝向target(可能在任一方向),所以我们想要target
  • \n
  • 如果value - step< value + step\xe2\x89\xa4 target,那么value可以采取一个完整的step向上走向target,所以我们想要value + step
  • \n
\n

在每种情况下,我们都需要中间值。

\n

测试表明,如果我们交换std::max操作数,GCC 会生成更少的两条指令,可能只是因为它与操作数恰好位于寄存器中的位置配合得更好:

\n
void move_towards(float &value, float target, float step) \n{\n    value = std::max(std::min(value + step, target), value - step);\n}\n
Run Code Online (Sandbox Code Playgroud)\n