45

要在两个变量之间进行线性插值ab给定一个分数f,我目前正在使用以下代码:

float lerp(float a, float b, float f) 
{
    return (a * (1.0 - f)) + (b * f);
}

我认为可能有一种更有效的方法。我使用的是没有 FPU 的微控制器,所以浮点运算是在软件中完成的。它们相当快,但它仍然需要 100 个周期才能相加或相乘。

有什么建议么?

nb 为了清楚上面代码中的等式,我们可以省略指定1.0为显式浮点文字。

4

7 回答 7

36

忽略精度差异,该表达式等效于

float lerp(float a, float b, float f)
{
    return a + f * (b - a);
}

那是 2 次加法/减法和 1 次乘法,而不是 2 次加法/减法和 2 次乘法。

于 2010-12-04T12:43:56.750 回答
9

如果您使用的是没有 FPU 的微控制器,那么浮点将非常昂贵。浮点运算很容易慢 20 倍。最快的解决方案是使用整数进行所有数学运算。

固定二进制点后的位数(http://blog.credland.net/2013/09/binary-fixed-point-explanation.html?q=fixed+binary+point)为:XY_TABLE_FRAC_BITS。

这是我使用的一个功能:

inline uint16_t unsignedInterpolate(uint16_t a, uint16_t b, uint16_t position) {
    uint32_t r1;
    uint16_t r2;

    /* 
     * Only one multiply, and one divide/shift right.  Shame about having to
     * cast to long int and back again.
     */

    r1 = (uint32_t) position * (b-a);
    r2 = (r1 >> XY_TABLE_FRAC_BITS) + a;
    return r2;    
}

使用内联函数,它应该是大约。10-20 个周期。

如果您有一个 32 位微控制器,您将能够使用更大的整数并获得更大的数字或更高的精度,而不会影响性能。此函数用于 16 位系统。

于 2014-01-27T11:11:45.753 回答
9

假设浮点数学是可用的,OP 的算法是一个很好的算法,并且a + f * (b - a)由于精度损失而总是优于替代方案,a并且b在幅度上存在显着差异。

例如:

// OP's algorithm
float lint1 (float a, float b, float f) {
    return (a * (1.0f - f)) + (b * f);
}

// Algebraically simplified algorithm
float lint2 (float a, float b, float f) {
    return a + f * (b - a);
}

在该示例中,假设 32 位浮点数lint1(1.0e20, 1.0, 1.0)将正确返回 1.0,而lint2将错误地返回 0.0。

当操作数的大小差异很大时,大部分精度损失出现在加法和减法运算符中。在上述情况下,罪魁祸首是 中的减法b - a和 中的加法a + f * (b - a)。由于组件在相加之前完全相乘,因此 OP 的算法不会受到此影响。


对于a=1e20, b=1的情况,这里是一个不同结果的例子。测试程序:

#include <stdio.h>
#include <math.h>

float lint1 (float a, float b, float f) {
    return (a * (1.0f - f)) + (b * f);
}

float lint2 (float a, float b, float f) {
    return a + f * (b - a);
}

int main () {
    const float a = 1.0e20;
    const float b = 1.0;
    int n;
    for (n = 0; n <= 1024; ++ n) {
        float f = (float)n / 1024.0f;
        float p1 = lint1(a, b, f);
        float p2 = lint2(a, b, f);
        if (p1 != p2) {
            printf("%i %.6f %f %f %.6e\n", n, f, p1, p2, p2 - p1);
        }
    }
    return 0;
}

输出,针对格式稍作调整:

    f lint1 lint2 lint2-lint1
0.828125 17187500894208393216 17187499794696765440 -1.099512e+12
0.890625 10937500768952909824 10937499669441282048 -1.099512e+12
0.914062 8593750447104196608 8593749897348382720 -5.497558e+11
0.945312 5468750384476454912 5468749834720641024 -5.497558e+11
0.957031 4296875223552098304 4296874948674191360 -2.748779e+11
0.972656 2734375192238227456 2734374917360320512 -2.748779e+11
0.978516 2148437611776049152 2148437474337095680 -1.374390e+11
0.986328 1367187596119113728 1367187458680160256 -1.374390e+11
0.989258 1074218805888024576 1074218737168547840 -6.871948e+10
0.993164 683593798059556864 683593729340080128 -6.871948e+10
1.000000 1 0 -1.000000e+00
于 2014-05-17T23:03:54.603 回答
3

如果您正在为没有浮点运算的微控制器编码,那么最好不要使用浮点数,而是使用定点算术

于 2010-12-04T13:12:29.573 回答
3

值得注意的是,标准线性插值公式 f1(t)=a+t(ba)、f2(t)=b-(ba)(1-t)、f3(t)=a(1- t)+bt 不保证在使用浮点运算时表现良好。即,如果 a != b,则不保证 f1(1.0) == b 或 f2(0.0) == a,而对于 a == b,不保证 f3(t) 等于 a , 当 0 < t < 1。

当我需要结果表现良好并准确命中端点时,此函数在支持 IEEE754 浮点的处理器上为我工作(我以双精度使用它,但浮点也应该工作):

double lerp(double a, double b, double t) 
{
    if (t <= 0.5)
        return a+(b-a)*t;
    else
        return b-(b-a)*(1.0-t);
}
于 2019-10-31T16:56:22.443 回答
2

从 C++20 开始,您可以使用std::lerp(),这可能是您的目标的最佳实现。

于 2020-03-03T12:56:59.933 回答
0

如果您希望最终结果为整数,那么使用整数作为输入可能会更快。

int lerp_int(int a, int b, float f)
{
    //float diff = (float)(b-a);
    //float frac = f*diff;
    //return a + (int)frac;
    return a + (int)(f * (float)(b-a));
}

这会进行两次强制转换和一次浮点数相乘。如果在您的平台上强制转换比浮点加/减更快,并且如果整数答案对您有用,那么这可能是一个合理的选择。

于 2010-12-04T13:14:23.127 回答