Wednesday, March 12, 2014

Beginner's Corner: Floating Point Numbers

Floating point numbers are useful (should I say mandatory) for scientific computations. But they’re also one of the most complex subject for programmers. From a purely theoretical point of view, they are the perfect illustration of the edge between discrete math (arithmetic, symbolic computation … ) and continuous math (math dealing with continuous values like real number and so on … ) Unfortunately for us, almost every scientific field use continuous math and only logic and computer science really care about discrete math.
Concretely, we have to represent values that have potentially infinite representation into a finite representation.

Floating Point Numbers Representation

There’s a standard describing how to encode real values in our finite world: floating point numbers (IEEE 754.)
You don’t need the details of the standard to do basic stuff (that should interesting for more advanced computations and optimization.) But you do need to understand the idea behind.
You’ve probably encounters values express in the form 1.234×10³. The idea is to encode in the exponent how large is number (that’s a generalization of unit used in the metric system.) Very large numbers will have a very important exponent and very small numbers a negative one. The exponent is some how moving the point in the number. Floating point numbers are based on the same idea, except that we’re on a computer and used power of two !
Representing real values that way let us have very high or very small value encoded using the same representation.
The literature is full of details and explanation about floating point, but the whole things you need is:
  • A float is a pair (m,e) (hey, I forgot the sign … ;)
  • m (the mantissa) is a number with a fixed representation
  • e (the exponent) is a signed integer (or biased-integer)

Issues ?

The main issue are often called absorption: small values may be absorbed by wider ones.
What does it mean ? Let’s go back to human readable stuff: we decide to have a fixed mantissa with four digits of the form X.XXX and use exponent of 10. Now you wan to compute 1×10⁰ + 1×10¯⁴,  the result should be 1.0001, but it requires five digits and thus we get as answer 1×10⁰.

The same apply to floating point numbers, let’s try in C:
#include <stdio.h>

int main() {
  float         x = 1.0, y = 1e-10;
  if (x+y == x)
    printf("y was absorbed !\n");
  else
    printf("y wasn' absorbed!\n");
  return 0;
}
This code will print the first message, of course !
That’s a classical one and you have probably seen it by yourself previously. But, this simple effect can have huge impact on your code and your results.

Vector Sum

A classical case where absorption effect may have strange result is array sum. Let’s take a simple example: we a huge array of float number and we want to compute the sum or the average value. We choose to fill our array is 1, so that the effect is more obvious.
#include <stdio.h>
#include <stdlib.h>

int main(int ac, char *av[]) {
  unsigned long size = 100000000;
  float         sum = 0.0, *tab;
  if (ac > 1) size = strtoul(av[1], NULL, 10);
  tab = malloc(size * sizeof (float));
  for (float *cur = tab; cur < tab + size; ++cur)
    *cur = 1.0;
  for (float *cur = tab; cur < tab + size; ++cur)
    sum += *cur;
  printf("size:\t%lu\nsum:\t%.8g\n", size, sum);
  return 0;
}
The output of this code is disturbing:
size:   100000000
sum:    16777216
The sum stop at some point: that’s a typical absorbing effect, when 1 becomes to small with regards to the current sum, the value stop to change. We can easily infer this value: the mantissa is represented using 24bits (in fact, it’s 23, there’s an implicit bit always set to 1 at the beginning), and 2²⁴ = 16777216 !
To be sure, let’s play with the array size:
size:   16777215
sum:    16777215

size:   16777216
sum:    16777216

size:   16777217
sum:    16777216
Can we solve this ? Yes, of course, the good solution is to perform a sum by chunks of the array. Here is two examples of implementation:
#define CHUNK_SIZE 65536

float splitted_sum(float *begin, float *end) {
  if (end - begin <= CHUNK_SIZE) {
  float               sum = 0.0;
  for (float *cur = begin; cur != end; ++cur)
    sum += *cur;
    return sum;
  }
  float                *mid = begin + (end - begin)/2;
  return splitted_sum(begin, mid+1) + splitted_sum(mid+1, end);
}

float splitted_sum2(float *begin, float *end) {
  float                 gsum = 0.0;
  for (float *chunk = begin; chunk < end; chunk += CHUNK_SIZE) {
    float               sum = 0.0;
    for (float *cur = chunk; cur != chunk + CHUNK_SIZE && cur != end; ++cur)
      sum += *cur;
    gsum += sum;
  }
  return gsum;
}
First, we should verify that we get a better result: the answer is yes, tested against the same context we get the awaited values:
size:   100000000
sum:    16777216
splitted_sum: 1e+08
splitted_sum2: 1e+08
Some words about that two functions: the global idea is to perform the sum piece by piece, i.e. we work on smaller sub-array so that individual values won’t get absorbed by the growing result.
The first version is a classic using recursive range division: you divide the current range until you reach the threshold, and only then do you do the array sum.
The second one is even simpler: two loops, one ranging over sub-arrays and the inner one doing the sum on each sub-array.
The positive point is that both solutions work for our case, but which one is better ? Good question ;)
Globally the recursive version is more robust: since we’re staging sums by pairs, most summed values will be in the same range, while the second version may encounter absorption (for my example, 100,000,000 of 1s, chunks smaller than 16 show absorption.)
What about performances ? I’ve made some quick tests … there isn’t any really differences. For small chunks (about 16 cells) the second version is really faster, but we’re also flirting with the error-zone. When around 32/64 the recursive version is faster (but not faster than the loop version with a chunk size of 16.) For bigger chunks, there’s no real differences. Here are some results (time added to previous output) for chunk size of 32:
size: 100000000
sum: 16777216 (time 0.0937701s) splitted_sum: 1e+08 (time 0.0697868s) splitted_sum2: 1e+08 (time 0.0825487s)
There’s another interesting point in building split sum: parallelism ! Moving to parallel here just requires computing in separate threads the sums of each chunk. It can be done by hand using threads library or stuff like parallel reduce of TBB …
Here is an example using C++11 std::async, (note that this is not the best way to do that) :
double simple_sum(double *begin, double *end) {
  double        r = 0;
  for (auto cur = begin; cur != end; ++cur)
    r += *cur;
  return r;
}

double async_sum(double *begin, double *end) {
  auto len = end - begin;
  if (len <= CHUNK_SIZE)
    return simple_sum(begin, end);
  auto mid = begin + len/2;
  auto part1 = std::async(async_sum, begin, mid);
  return async_sum(mid, end) + part1.get();
}
This implementation spawn too many threads and consume too much memory. A good parallel implementation should separate the parallel splitting and the splitting of the array. And note that the std::async and the std::future is too expensive to (at least in current implementation.)

Approximation And Testing

Another consequence of floating point approximation is the instability of computation upon expression re-ordering (i.e. using associativity and commutativity.)
Let’s take a simple example. Here are implementations of the factorial function:
double fact(int n) {
  if ( n < 2 ) return 1;
  return n * fact(n - 1);
}

double fact_term(int n, double a) {
  if ( n < 2 ) return a;
  return fact_term(n - 1, n * a);
}
They are classical examples of recursive implementations of the factorial. Here is a simple test:
int main() {
  int           n = 51;
  double        r0, r1, r2;
  r0 = fact(n);
  r1 = fact_term(n, 1);
  printf("fact(%d) = r0 = %g\n", n, r0);
  printf("fact_term(%d) = r1 = %g\n", n, r1);
  printf("r1 - r0 = %g\n", r1 - r0);
  return 0;
}
The output explains the problem itself:
fact(51) = r0 = 1.55112e+66
fact_term(51) = r1 = 1.55112e+66
r1 - r0 = -5.61217e+50
That’s make a huge difference !
Just to be sure, I tracked the moment when the errors shows up, let’s look at the same code with 50 rather 51 …
fact(50) = r0 = 3.04141e+64
fact_term(50) = r1 = 3.04141e+64
r1 - r0 = 0
No more errors !
No you understand the difficulty ? Errors in floating point can comes quickly for a simple reordering and can disappear with certain range of values. Our main issue is to be able to test various implementations of the same computation.
As an example, I’ll take a Heron based square root and compare it with the result of the standard library function sqrt(3).
// Simple Heron method for square root
// threshold indicate when to stop refinement
double mysqrt(double x, double threshold) {
  double        r0 = x, r1 = 0;
  while (fabs(r0 - r1) > threshold) {
    r1 = r0;
    r0 = (r0 + x/r0)/2;
  }
  return r0;
}
The method works by iterative refinement, at each step we try to narrow our range around the square root. We stop as soon as the difference between the current value and the next one falls under the given threshold. Our purpose is to find a good threshold, for that I’ll compute sqrt(2, th) with increasing threshold and stop when the difference between my function and the standard function is stable … Just look at the code:
int main() {
  double        x = 2, my_root, c_root;
  double        th = 1, diff=HUGE_VAL, prev_diff, mem = 0;
  unsigned      c = 0;
  c_root = sqrt(x);
  do {
    prev_diff = diff;
    my_root = mysqrt(x,th);
    diff = fabs(my_root - c_root);
    if (prev_diff - diff <= 0) {
      if (c == 0) mem = th;
      c += 1;
    } else c = 0;
    th = th/8;
  } while (prev_diff - diff > 0 || c < 4);

  printf("Threshold: %g - %a\nDiff: %g\n", mem, mem, diff);
  return 0;
}
Ok, the code is more a script in C than a real testing program, but you can notice the tricks: we won’t get the same result, unless we use exactly the same algorithm (implemented the same way … ) So, we compute the difference between the expected result and our own value, it appears that we can’t do better than something around 2.22×10¯²². Here is the output:
Threshold: 5.96046e-08 - 0x1p-24
Diff: 2.22045e-16

Real float format ?

A float (32bit floating point numbers) :

f = (-1^S) × 1.SIGNIFICAND × 2^(EXPONENT - 127)

These elements are stored in that way:


As you may notice, the one before the point is not stored, the idea is that the mantissa is scaled such that it start with a one (remember, we’re in binary, that’s not a problem.)
The main consequences it’s some times difficult to compare values and non-strict comparing are sometime fuzzy. That’s a complex field of study and still a problem for many applications.
A funny consequence of the format is the existence of two zero ! The sign in the representation is an independent bit, not like in signed integer and thus we can all the value set to zero but the sign bit set to one ! A classical example is this absolute value function:
float fabsf(float x) {
  if (x > 0.0) return x;
  if (x < 0.0) return x;
  return 0.0;
}

Conclusion

Dealing with floating point numbers is not easy task, there’s a lot of traps, and you haven’t seen all here, I’m not a scientific computation specialist, I just report my simple experience.

Saturday, March 8, 2014

Playing With C …

Most of the time, when you're interested in C programming, you're also interested in performances. Doing homemade micro-benchmarks is, in itself, an interesting activity even if it's not productive at all …

Recently, after reading an article about pipeline and optimization (Playing with the CPU pipeline), I decided to test that by myself. This was also the occasion to (re)discover classical math functions.

Computing sinus

The easiest way to compute a trigonometric function is to use the traditional Taylor decomposition, there's better way, but we're not here to provide the best sinus function, we just want to play around …

I'll use the same approximation as my starting articles (since I want to test the code in it … ) The first naive version uses a homemade power and factorial functions.

uint64_t fact(uint64_t n) {
  uint64_t      r = 1;
  for (; n; --n) r *= n;
  return r;
}

double qpower(double x, int p) {
  return p == 1 ? x : qpower(x * x, p >> 1) * (p % 2 ? x : 1);
}

// Taylor version
double sin_taylor(double x) {
  double        r = x;
  int           s = -1;
  for (int i = 3; i < 16; i += 2, s *= -1)
    r += s * qpower(x, i) / (double)fact(i);
  return r;
}

I hope you know how a quick exponentiation works (it has a O(log p) complexity).

The main issue here is that we're computing several time constant values using a function. So, the next step is to pre-compute the 8 values (I'll use the approximation provides in the aforementioned article that are closer to minimax approximation, which yield more accurate results) and put them in an array (for easier access.)

static const double Coef[] = {
  +1.0,
  -1.666666666666580809419428987894207e-1,
  +8.333333333262716094425037738346873e-3,
  -1.984126982005911439283646346964929e-4,
  +2.755731607338689220657382272783309e-6,
  -2.505185130214293595900283001271652e-8,
  +1.604729591825977403374012010065495e-10,
  -7.364589573262279913270651228486670e-13,
};

Now we can remove the call to fact (and the division by the way):

double sin_taylor2(double x) {
  double        r = x;
  for (int i = 3; i < 16; i += 2)
    r += qpower(x, i) * Coef[i >> 1];
  return r;
}

Next optimization deals with the exponentiation: the goal is to rearrange and factorize in order to have less multiplication and try take advantage of the pipeline and out-of-order parallelism. I chose this version (spoil: it will give us the best execution time) upon all variation presented in the original paper:

double sin_v6(double x) {
  double        x2 = x * x;
  double        x4 = x2 * x2;
  double        x8 = x4 * x4;
  double        A = Coef[0] + x2 * (Coef[1] + x2 * (Coef[2] + x2 * Coef[3]));
  double        B = Coef[4] + x2 * (Coef[5] + x2 * (Coef[6] + x2 * Coef[7]));
  return x * (A + x8 * B);
}

Is it faster ? It's time to test !

Measuring Performances ?

How can we measure correctly execution time ? We don't have good solutions, the only way is to find an accurate clock (probably provided by the system) take its value before and after calling our function and computing the difference.

I choose the POSIX clock_gettime(2) function with CLOCK_MONOTONIC which seems suited for our job. This will look like that:

  struct timespec       c0, c1;
  clock_gettime(CLOCK_MONOTONIC, &c0);
  // Do your dirty work here
  clock_gettime(CLOCK_MONOTONIC, &c1);

OK, that's how we do usually. To compute the difference between clocks, we have to take a look at the timespec structure since operations like timersub(3) operate on timeval structure not timespec (timeval provides microseconds while timespec provides nanoseconds … )

According to the man-page, timespec contains (at least) two fields, one with the seconds, the other with the nanoseconds. The simplest way to compute difference is to convert timespec into double float:

double spec_to_double(struct timespec *ts) {
  return ts->tv_sec + 1e-9 * ts->tv_nsec;
}

I prefer writing a function, and then tag it as static inline rather than directly doing computation in the code, it's much more readable and the compiler is able to optimize this correctly. Never be afraid of functions, they are better documentation than any comments.

OK, I'm now able to measure execution time, normally with a precision of a nanosecond. I don't really believe it: what is the cost of calling clock_gettime(2) ? According to its manual section, its a system call. And, is my function slow enough to be measured ?

I got several answers to that:

  • Take some measures ! We'll just measure time between two consecutive call to clock_gettime(2)
  • Don't run your function only once ! We'll call our function about thousands or more time, to obtain some average …
  • Run your test several time, if the result is stable, it may have some meaning.
So, I wrote a small piece of code that compute the time between two consecutive calls of clock_gettime(2), then the same with a call to the libc sin function. I run that 10 times and compute an average. I also print intermediary results. Here is the code:

#define _XOPEN_SOURCE 500

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

static inline
double spec_to_double(struct timespec *ts) {
  return ts->tv_sec + 1e-9 * ts->tv_nsec;
}

int main() {
  struct timespec       c0, c1;
  double                r, s0, s1, a=0;

  for (int i = 0; i < 10; ++i) {
    clock_gettime(CLOCK_MONOTONIC, &c0);
    clock_gettime(CLOCK_MONOTONIC, &c1);
    s0 = spec_to_double(&c1) - spec_to_double(&c0);

    clock_gettime(CLOCK_MONOTONIC, &c0);
    r = sin(1.5);
    clock_gettime(CLOCK_MONOTONIC, &c1);
    s1 = spec_to_double(&c1) - spec_to_double(&c0);

    a += s1 - s0;
    printf("> %g\n", s1 - s0);
  }
  printf("%g\n", a / 10);
  return 0;
}

I ran that piece of code on a i7 3770 compiled with clang and -O0 optimization flag and get the following result:

> 1.034e-05
> 3.35043e-07
> 7.19447e-08
> 6.07688e-08
> -1.81608e-08
> 7.0082e-08
> 9.19681e-08
> 1.28057e-07
> 9.17353e-08
> 8.68458e-08
1.12583e-06

Wait, what was that ? I've got a negative result … This is exactly what I was looking for: our clock is not sufficiently precise and sometimes (Ok, that was not the first run, but it happens say 1 time out of 3) the time without the sin is longer.

There's also this first value, which is often longer. There's probably a rational for that related to processor cache (the first time the code of sin need to be copied from library to the cache, and it's small enough to fit in and stay there for the other run.)

Anyway, those values are not interesting, they only show the fact that we're not able to perform precise measures, but we can use loops. Replacing the call to sin by a loop performing the same line thousand of time yield the following results:

> 3.72389e-05
> 4.90241e-05
> 4.39051e-05
> 4.30089e-05
> 4.2367e-05
> 4.86639e-05
> 4.08019e-05
> 3.8858e-05
> 3.86741e-05
> 3.90329e-05
4.21575e-05

Ok, that's a little bit more stable, but we still got a variation of about 20 or 25% of the measured time ! Adding a zero, give us a more accurate result:

> 0.00025304
> 0.000266933
> 0.000270017
> 0.000247684
> 0.000247303
> 0.000247422
> 0.000249467
> 0.000247646
> 0.000248894
> 0.000247764
0.000252617

Let's Go !

So, I'm now able to do all my tests. I've implemented almost all versions presented in the original article with some minor variations. And since I was planning to see the interaction with compiler optimizations, my first runs was compiled using gcc and -O3, then clang and -O3 also. Let's have a look at the result:
  • Empty Loop: just a loop doing sum in a variable
  • libm sin: sin from C math library
  • sin_taylor: the first Taylor series version
  • sin_taylor2: the same but with constant rather than call to fact
  • sin_v6: high/low factor split

CompilerEmpty Looplibm sinsin_taylorsin_taylor2sin_v6
gcc -O30.23550350801.46981147893.88085019010.60061714310.3628679269
clang -O30.23594911191.48031646397.70664868694.29427291900.3918996351
gcc version:    4.8.2 20140206 (prerelease) (Arch Linux package)
clang version:  3.4 (tags/RELEASE_34/final) (Arch Linux package)

Each run corresponds to 100,000,000 calls to the corresponding sinus function. The loop is also accumulating the errors (variation) against standard sin function (libm.) We use a pre-filled vector of random float numbers to avoid accounting the cost of the random generator.

Nice results, isn't it ? But, I cheated a little bit, my first version yielded strange timing, so let's see why and how I obtain these results.

Strange Timing


In a previous version, I got the following values:

CompilerEmpty Looplibm sinsin_taylorsin_taylor2sin_v6
gcc -O30.23170267600.23144390110.23148966420.23143743000.2321709669
clang -O30.23139798321.42392848320.23179650800.23211171990.2316789902

Almost all timing was identical, and worst, it was similar to the empty loop.

I made two errors here:
  • rather than having a vector of random float, I used the same float for all calls
  • I forgot the builtin functions of gcc
What's the deal with benching with the same value ? All my functions are in my main file and with -O3 optimization, the compiler is able to understand that the expression is constant in the loop and compute it just one time. Fail.

The second error is about testing the libm function: it seems that gcc is able to optimize it the same way as local functions, while clang runs it at each loop step. Why ? It takes me sometimes to realize what happens.

The traditional way of solving this kind of issues is to take a look at the generated code (assembly) and try to understand how your functions are called. My testing code is rather ugly and not factorized at all, combine with -O3, it becomes very difficult to find the difference. In that case, there's a better test: the option -fno-builtin !

Yeah … gcc has a builtin function for sinus and thus it can safely do constant propagation to avoid computing the sinus at each step !

So, when comparing your code against standard functions, you should deactivate builtin function most of the time in order to have coherent results.

Compiler Optimization Impact


Running your code in -O3 is nice, things seems to go faster, OK. But what is the real impact is your code really efficient ? You should consider running your code with various level of optimization to see the differences. Here is the same example with optimization ranging from -O0 to -O3, using gcc and clang. I also add -ffast-math (I know, it may yield altered floating points results.)

Compilerlibm sinsin_taylorsin_taylor2sin_v6
gcc -O01.640969305123.357415701010.57552155781.2111645590
gcc -O11.523625704011.47179266305.86691553100.4126090182
gcc -O21.47107294508.22070184285.28496960290.4066506480
gcc -O31.46981147893.88085019010.60061714310.3628679269
gcc -O3 -ffast-math1.47539282793.76906145810.50010442610.3611039480
clang -O01.561618749126.048279444911.43707089411.0181730469
clang -O11.48056166088.90578887804.73330464190.4122427311
clang -O21.47776968387.77558478694.44361459300.3908856600
clang -O31.48031646397.70664868694.29427291900.3918996351
clang -O3 -ffast-math1.47998775197.68622586114.29045542800.3916952400

Small gains on the first column, come probably from loop optimizations (clang does not optimize it, and I deactivate builtin for gcc.) Tests for the empty loop (not shown here) yield a globally constant time for all compiler's options. Which tends to enforce our testing process: the loop in itself can safely be removed from execution time.

Another interesting point is the average error of our functions. I choose to use the standard sin function from libm as a referent. Globally, sin_v6 has an average error around of -9.60791e-23 (on the range -π/2 to π/2.) Even with -ffast-math, it seems that this version output a result with the same average precision.

It could be interesting to investigate a little bit more on precision, my actual version is not a real minimax polynomial approximation, but the global code is quite the same and we should have similar performances. If you're interest on this precision topic, there's another article on the same website as the aforementioned article: Better function approximations: Taylor vs. Remez.

Conclusion

Ok, what do we got:
  • Before evaluating performances, you should be sure of your tools for measuring.
  • Beware of your compiler's impact: builtin functions and optimization may invalidate some of your results.
  • Even if the compiler is better than you for most optimization, a clever code can be faster.
  • Yes, you can have an homemade math function performing better than the corresponding libc function (but only for a reduced range … )

Bonus

You may ask why haven't you wrote an assembly version ? Good question, here it is an implementation using Intel FPU instruction FSIN:

double sin_v7(double x) {
  double        r;
  __asm (
         "fldl %1\n"
         "fsin\n"
         "fstpl %0\n"
         : "=m"(r): "m"(x));
  return r;
}

I've test it using the same framework, and guess what ? It's slower !

For the same condition (hundreds of million calls) I get a time of 2.87384s which is nearly ten time slower than the sin_v6 function !

You want to know why ? That's probably due to the way FPU works: it induce a context switch just for one operation, so even if the operation itself is faster, the function is inefficient …