54

To do a linear interpolation between two variables a and b given a fraction f, I'm currently using this code:

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

I think there's probably a more efficient way of doing it. I'm using a microcontroller without an FPU, so floating point operations are done in software. They are reasonably fast, but it's still something like 100 cycles to add or multiply.

Any suggestions?

n.b. for the sake of clarity in the equation in the code above, we can omit specifying 1.0 as an explicit floating-point literal.

waterproof
  • 4,943
  • 5
  • 30
  • 28
Thomas O
  • 6,026
  • 12
  • 42
  • 60

7 Answers7

43

As Jason C points out in the comments, the version you posted is most likely the best choice, due to its superior precision near the edge cases:

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

If we disregard from precision for a while, we can simplify the expression as follows:

    a(1 − f) × (ba)
 = aaf + bf
 = a + f(ba)

Which means we could write it like this:

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

In this version we've gotten rid of one multiplication, but lost some precision.

aioobe
  • 413,195
  • 112
  • 811
  • 826
  • 1
    The original algorithm is also not much of a loss performance-wise: FP multiplication is much faster than FP addition, and if `f` is guaranteed to be between 0 and 1, certain optimizations to `(1-f)` are possible. – Sneftel May 17 '14 at 23:29
  • 1
    @Sneftel: Can you elaborate on the optimizations for `1 - f`? I happen to be in that situation and am curious :D – Levi Morrison Sep 11 '14 at 01:54
  • 1
    @LeviMorrison Basically, that guarantees that the exponent for `f` is nonpositive (and of course guarantees fixed values for the mantissa and exponent of `1`), cutting out most of the branches in the subtraction routine. – Sneftel Sep 11 '14 at 08:18
  • @JasonC About precision loss: there are cases where we use an `f` which is not necessarly between [0.0, 1.0] (e.g. parametric definition of a line). Which of the original `lerp` and this one would be more precise when e.g. `f` is large enough relatively to `b`, `a`, or maybe `b - a` ? – coredump Feb 24 '15 at 09:42
  • 2
    @coredump Sorry about not noticing your comment 2 years ago (heh...). The OP's would still be more precise, in particular, if `f * (b - a)` is significantly different in magnitude than `a` in this algorithm then the addition falls apart. It's the addition/subtraction where you run into trouble. That said even the OP's can fail if `f` is too large relative to `1.0f`, as `1.0f - f` could become equivalent to `-f` for very large `f`. So if you're working with huge values for `f` you'll need to think hard about the math a bit. The issue is you run into things like `1.0 + 1.0e800 == 1.0e800`. – Jason C Mar 20 '17 at 00:48
  • 3
    Just think of floating-point numbers as fixed-point mantissas and an exponent (it's more complicated than that but viewing them this way is *sufficient* to spot *many* trouble areas). So if you're exceeding the precision of the mantissa, you'll start to lose information. Conceptually similar to the fact that we cannot, for example, represent 1,230,000 in decimal with only two significant digits (1.2 * 10^6 is the closest we can get), so if you do 1,200,000 + 30,000 but you only have two significant digits at your disposal, you lose that 30,000. – Jason C Mar 20 '17 at 00:55
  • @JasonC Thanks! I forgot about that question :-) – coredump Mar 20 '17 at 12:57
  • 2
    While it may often be the better choice, the OP's algorithm is not _always_ the better choice. Consider the case when `a == b`: this algorithm will always return the correct answer, but depending on the value of `t`, OP's algorithm can lose precision in both the left-hand and right-hand side of the addition, and it won't add up to the initial value. – nemetroid Apr 29 '20 at 21:21
9

If you are on a micro-controller without an FPU then floating point is going to be very expensive. Could easily be twenty times slower for a floating point operation. The fastest solution is to just do all the math using integers.

The number of places after the fixed binary point (http://blog.credland.net/2013/09/binary-fixed-point-explanation.html?q=fixed+binary+point) is: XY_TABLE_FRAC_BITS.

Here's a function I use:

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;    
}

With the function inlined it should be approx. 10-20 cycles.

If you've got a 32-bit micro-controller you'll be able to use bigger integers and get larger numbers or more accuracy without compromising performance. This function was used on a 16-bit system.

waterproof
  • 4,943
  • 5
  • 30
  • 28
Jim Credland
  • 101
  • 1
  • 2
  • 1
    I read the website but am still a little confused at what position should be. Is this a value of 0 to 0xFFFF? or 0 to 0xFFFE? Also what is XY_TABLE_FRAC_BITS? 8? – jjxtra Apr 05 '17 at 22:15
  • @jjxtra: `XY_TABLE_FRAC_BITS` is just the (poorly) named integer constant whose value specifies where the assumed binary point is in the fixed point integer values being used (since it doesn't "float" around in them as it does in floating-point numbers). – martineau Oct 17 '18 at 18:45
9

Presuming floating-point math is available, the OP's algorithm is a good one and is always superior to the alternative a + f * (b - a) due to precision loss when a and b significantly differ in magnitude.

For example:

// 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);
}

In that example, presuming 32-bit floats lint1(1.0e20, 1.0, 1.0) will correctly return 1.0, whereas lint2 will incorrectly return 0.0.

The majority of precision loss is in the addition and subtraction operators when the operands differ significantly in magnitude. In the above case, the culprits are the subtraction in b - a, and the addition in a + f * (b - a). The OP's algorithm does not suffer from this due to the components being completely multiplied before addition.


For the a=1e20, b=1 case, here is an example of differing results. Test program:

#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;
}

Output, slightly adjusted for formatting:

    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
waterproof
  • 4,943
  • 5
  • 30
  • 28
Jason C
  • 38,729
  • 14
  • 126
  • 182
  • 4
    Interestingly, OP's version is not always superior. I thought it was then got bitten by this example: `lerp(0.45, 0.45, 0.81965185546875)`. It obviously should give 0.45, but at least for double precision I get 0.45000000000000007 whereas clearly the a + (b-a)*f version gives a when a==b. I'd love to see an algorithm that has the property that `lerp(a, b, f)` returns `a` if `f==0`, `b` if `f==1`, and stays in the range [`a`,`b`] for `f` in [0,1]. – Ben Dec 22 '16 at 15:22
  • 3
    First, you need the case `if a == b -> return a`. However, exactly 0.45 is impossible to represent in double or floating point precision as it is not an exact power of 2. In your example, all parameters `a, b, f` are stored as double when inside the function call – returning `a` would never return exactly 0.45. (In the case of explicitly typed languages like C, of course) – Benjamin R Mar 20 '17 at 00:29
  • This looks like the better choice. Interestingly though, standard library lerp seems to be going with the [algebraically simplified version](https://en.cppreference.com/w/cpp/numeric/lerp). Thoughts? – Lorah Attkins Sep 03 '21 at 09:55
  • @BenjaminR I think Ben's point is that `lerp(a, a, anything)` should return `a`, which fails in the case of lerp(0.45, 0.45, 0.81965185546875) with OP's implementation. The fact that `a` here is not exactly decimal 0.45 isn't really relevant. – Don Hatch Oct 19 '21 at 00:03
  • 1
    @Don Well; the fact is relevant because it is the crux of Ben's observation; what's been overlooked is that its connection to the lerp implementation is a red herring: Yes `lerp(a, a, anything)` should return `a`, but 0.45 can't be represented and is therefore *outside the domain of that function*, and so it does not make sense to talk about it. Note also that both versions of lerp would yield not exactly 0.45. Even `return 0.45` would not return 0.45. Programmers using such languages generally don't mention this in conversation, though, because it's usually implicit and uninteresting. – Jason C Oct 19 '21 at 00:33
  • 1
    @JasonC Restating Ben's observation in a different way, in order to avoid the red herring about 0.45 not being in the domain: let a = 0.450000000000000011102230246251565404236316680908203125 and t = 0.819651855468749968025576890795491635799407958984375 . Both a and t are exactly representable in double precision (and are what you get when you say 0.45 and 0.81965185546875 respectively in a C program). lerp(a, a, t) should give back exactly a, but it doesn't when using OP's implementation. So this is a counterexample to your claim that OP's algorithm is always superior to the alternative. – Don Hatch Oct 19 '21 at 07:16
  • @Don I see. That is a very valid example. I am not in a place to verify that result right now, but assuming what you say is correct, then it is relevant to my answer (i.e. precision problems that can be managed), and there is no red herring. So I will test later when I get a chance, and update the answer if needed. – Jason C Oct 19 '21 at 13:42
  • 1
    @LorahAttkins while the C++ standard specifies `std::lerp` as computing $a+t(b-a)$, that is _only_ used as a mathematical definition for what the function computes. The standard additionally puts more restrictions on the implementation of `std::lerp`: it must be monotonic, it must be precise for $t\in\{0,1\}$ and $a = b$. This means neither `lint1` nor `lint2` are valid implementations of `std::lerp`. As such nobody is going to use `std::lerp` because it's too branchy and slow. – CAD97 Nov 19 '21 at 22:06
5

It is worth to note, that the standard linear interpolation formulas f1(t)=a+t(b-a), f2(t)=b-(b-a)(1-t), and f3(t)=a(1-t)+bt do not guarantee to be well-behaved when using floating point arithmetic. Namely, if a != b, it is not guaranteed that the f1(1.0) == b or that f2(0.0) == a, while for a == b, f3(t) is not guaranteed to be equal to a, when 0 < t < 1.

This function has worked for me on processors that support IEEE754 floating point when I need the results to behave well and to hit the endpoints exactly (I use it with double precision, but float should work as well):

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);
}
0kcats
  • 672
  • 5
  • 16
  • In c++20 they added std::lerp, which guarantees monotonic behavior. – 0kcats Dec 03 '20 at 23:16
  • This seems to be the best solution I've ever seen. I'd like to see a proof that it's monotonic. (It seems to be, in that I can't find a counterexample, but I don't see why.) – Don Hatch Oct 19 '21 at 07:23
  • @DonHatch Changed the wording as you requested. Thanks! – 0kcats Oct 25 '21 at 06:01
  • @DonHatch I've removed "monotonic" from the answer for now as I have no proof of that. – 0kcats Oct 25 '21 at 06:26
  • Oh, but the monotonicity is the best part! :-) Clearly the two pieces f1 and f2 are monotonic, it remains to be shown that it's monotonic at the switch point t=0.5. I think it is (just from the fact that my search for a counterexample failed), just haven't proved it yet. Maybe this would be a good question for some other more theoretic-minded site such as cs.stackechange.com. Note that there's a related question there: https://cs.stackexchange.com/questions/59625/numerical-stability-of-linear-interpolation – Don Hatch Oct 25 '21 at 11:15
3

If you're coding for a microcontroller without floating-point operations, then it's better not to use floating-point numbers at all, and to use fixed-point arithmetic instead.

Gareth Rees
  • 64,967
  • 9
  • 133
  • 163
3

Since C++20 you can use std::lerp(), which is likely to be the best possible implementation for your target.

tambre
  • 4,625
  • 4
  • 42
  • 55
  • 2
    `std::lerp` should be used exactly nowhere in my opinion. Very rarely do you actually need both interpolation *and* extrapolation, plus a ton of branching behavior, *on top of* the numerically unstable internal implementation. I have so many disagreements with how `std::lerp` was implemented, it's difficult to recommend. – jeremyong Sep 22 '20 at 00:35
  • 1
    @jeremyong can you give an example of a case where `std::lerp` does poortly? Its contract certainly looks like a good one in several important ways: it's monotonic, lerp(a,b,0)==a, lerp(a,b,1)==b (and those two facts implies it stays in range [a,b] for t in [0,1]), lerp(a,a,t)==a. So the usual complaints seem to be covered. – Don Hatch Oct 19 '21 at 00:34
0

If you want to the final result to be an integer, it might be faster to use integers for the input as well.

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));
}

This does two casts and one float multiply. If a cast is faster than a float add/subtract on your platform, and if an integer answer is useful to you, this might be a reasonable alternative.

waterproof
  • 4,943
  • 5
  • 30
  • 28
mtrw
  • 34,200
  • 7
  • 63
  • 71
  • For `f * (b - a)`, the type promotion will grant that `(b - a)` is promoted to `float` because `f` is of type `float`. So, the explicit cast to `(float)` in `(float)(b - a)` is at best illustrative but actually not necessary, isn't it? – Scheff's Cat Apr 01 '20 at 16:12
  • 1
    @Scheff - yes you're correct, the float cast is written out purely to draw attention to something the compiler will insert anyway. – mtrw Apr 01 '20 at 19:04