9

I'm trying to find ex without using math.h. My code gives wrong anwsers when x is bigger or lower than ~±20. I tried to change all double types to long double types, but it gave some trash on input.

My code is:

#include <stdio.h>

double fabs1(double x) {
    if(x >= 0){
        return x;
    } else {
        return x*(-1);
    }
}

double powerex(double x) {
    double a = 1.0, e = a;
    for (int n = 1; fabs1(a) > 0.001; ++n) {
        a = a * x / n;
        e += a;
    }
    return e;
}

int main(){
    freopen("input.txt", "r", stdin);
    freopen("output.txt", "w", stdout);
    int n;
    scanf("%d", &n);
    for(int i = 0; i<n; i++) {
        double number;
        scanf("%lf", &number);
        double e = powerex(number);
        printf("%0.15g\n", e);
    }
    return 0;
}

Input:

8
0.0
1.0
-1.0
2.0
-2.0
100.0
-100.0
0.189376476361643

My output:

1
2.71825396825397
0.367857142857143
7.38899470899471
0.135379188712522
2.68811714181613e+043
-2.91375564689153e+025
1.20849374134639

Right output:

1
2.71828182845905
0.367879441171442
7.38905609893065
0.135335283236613
2.68811714181614e+43
3.72007597602084e-44
1.20849583696666

You can see that my answer for e−100 is absolutely incorrect. Why does my code output this? What can I do to improve this algorithm?

mkrieger1
  • 19,194
  • 5
  • 54
  • 65
  • Modify the code to print the values of `n` and `a` as it goes through the loop, then run it for the −100 case and see what the values look like. See how big they get. What resolution does the `double` format have? How accurately can the sums be represented when the numbers get that big? What size error should you expect? – Eric Postpischil Dec 16 '21 at 16:23
  • Run with a debugger, or add intermediate prints in your calculating loop, and see when the value of `a` is starting to deviate from the expectation. – Eugene Sh. Dec 16 '21 at 16:25
  • Could there be `int` overflow? – Alex Guteniev Dec 16 '21 at 16:27
  • GCC has builtin functions `__builtin_expf` and `__builtin_expd` you can call, [as documented here](https://gcc.gnu.org/onlinedocs/gcc/Other-Builtins.html), but I guess your questions is really more about doing calculations yourself instead of avoiding a specific header file. And those functions just seem to call the C library under the hood anyway, instead of doing something more direct. – David Grayson Dec 16 '21 at 16:38
  • FYI https://stackoverflow.com/questions/49960020/with-which-algorithm-exponential-functions-are-computed-in-the-gnu-c-standard – mkrieger1 Dec 16 '21 at 16:40
  • Also interesting: https://stackoverflow.com/questions/6984440/approximate-ex – mkrieger1 Dec 16 '21 at 16:41
  • It's just a limit of this kind of approximation. Try to inspect the values and their progression: https://godbolt.org/z/85T95nM3K – Bob__ Dec 16 '21 at 16:45
  • 1
    Here `for (int n = 1; fabs1(a) > 0.001; ++n) {`, you impose that the approximation is generally not better than 0.001 – Damien Dec 16 '21 at 16:49
  • OP, take a look at asymptotic expansions for large (positive or negative) arguments. It's probably covered in "Handbook of Mathematical Functions" by Abramowitz and Stegun, which I think you can find on the web. – Robert Dodier Dec 17 '21 at 00:29
  • An absolute (and arbitrary) test like `fabs1(a) > 0.001` is always suspicious. –  Dec 17 '21 at 13:23

2 Answers2

5

When x is negative, the sign of each term alternates. This means each successive sum switches widely in value rather than increasing more gradually when a positive power is used. This means that the loss in precision with successive terms has a large effect on the result.

To handle this, check the sign of x at the start. If it is negative, switch the sign of x to perform the calculation, then when you reach the end of the loop invert the result.

Also, you can reduce the number of iterations by using the following counterintuitive condtion:

e != e + a

On its face, it appears that this should always be true. However, the condition becomes false when the value of a is outside of the precision of the value of e, in which case adding a to e doesn't change the value of e.

double powerex(double x) {
    double a = 1.0, e = a;
    int invert = x<0;
    x = fabs1(x);
    for (int n = 1; e != e + a ; ++n) {
        a = a * x / n;
        e += a;
    }
    return invert ? 1/e : e;
}

We can optimize a bit more to remove one loop iteration by initializing e with 0 instead of a, and calculating the next term at the bottom of the loop instead of the top:

double powerex(double x) {
    double a = 1.0, e = 0;
    int invert = x<0;
    x = fabs1(x);
    for (int n = 1; e != e + a ; ++n) {
        e += a;
        a = a * x / n;
    }
    return invert ? 1/e : e;
}
dbush
  • 205,898
  • 23
  • 218
  • 273
3

For values of x above one or so, you may consider to handle the integer part separately and compute powers of e by squarings. (E.g. e^9 = ((e²)²)².e takes 4 multiplies)

Indeed, the general term of the Taylor development, x^n/n! only starts to decrease after n>x (you multiply each time by x/k), so the summation takes at least x terms. On another hand, e^n can be computed in at most 2lg(n) multiplies, which is more efficient and more accurate.

So I would advise

  • to take the fractional part of x and use Taylor,
  • when the integer part is positive, multiply by e raised to that power,
  • when the integer part is zero, you are done,
  • when the integer part is negative, divide by e raised to that power.

You can even spare more by considering quarters: in the worst case (x=1), Taylor requires 18 terms before the last one becomes negligible. If you consider subtracting from x the immediately inferior multiple of 1/4 (and compensate multiplying by precomputed powers of e), the number of terms drops to 12.

E.g. e^0.8 = e^(3/4+0.05) = 2.1170000166126747 . e^0.05

  • If you add constants, consider using `ln2`, then decompose `x=m*ln2+u`, `abs(u)<=0.5*ln2<0.35`, so that the result is `2^m*exp(u)`. Otherwise the idea would be to compute `y=exp(x/2^n)` and recover `exp(x)` be `n` squarings of `y`. – Lutz Lehmann Dec 22 '21 at 14:00
  • @LutzLehmann: squarings of `exp(x/2^n)` won't work, due to the truncation errors. –  Dec 22 '21 at 14:52
  • But you propose to compute the bulk of the result from squarings of `e`. – Lutz Lehmann Dec 22 '21 at 15:10
  • @LutzLehmann: yes, this is safe, not squarings from numbers like 1.000000001, If I am right. –  Dec 22 '21 at 15:41
  • The idea is of course to have `x/2^n` in some moderate range, `0.5 to 1` as example, so that the power series or polynomial approximation works reasonably fast and the squaring does not impact the accuracy too much. This method is indeed used in the math library of the `bc` multiprecision command-line tool, arguably with rising the working precision to account for the accuracy loss of the squaring "trick". – Lutz Lehmann Dec 22 '21 at 17:06