NSIMD documentation
Index | Tutorial | FAQ | Contribute | API overview | API reference | Wrapped intrinsics | Modules

How tests are done?

First and foremost note that this is a work in progress and that we are doing our best to have serious testing of the library.

We can also state our conclusion on testing: we are not and never will be satisfied with our tests, there are not enough of them, we want more.

The current system has on average 10000 tests by SIMD extensions. Thanks to our "Python" approach we can automatically generate tests for all operators and for all types. This has greatly helped us in finding bugs. But, as you know, bugs are always there.

Why write this?

Testing the library has been taken seriously since its very beginning. Tests have gone through several stages:

Tests on floatting points are done using ULPs. ULP means units in the last place and is commonly used for the comparison of floatting point numbers. It is in general a bad idea to compare floats with the == operators as it essentially compares bits. Instead we want to check if the results of two computations are "not to far away from each other". When checking an operator, let's say, on CPUs and GPUs, we to take into account that

ULPs

This chapter is dedicated to math proof concerning ULPs. Indeed people use this notion but proofs are hard to find. We give our own definition of distance in ULPs, compare it to the usual one and give pros and cons. We assume the reader is familiar with basic mathematics.

For this entire chapter fix the following:

A floatting point number is an element of \(\mathbb{R}\) of the form \(m b^e\) with \(e \geq -M\) and \(m \in \mathbb{Z}\). More precisely we define the set of floatting point numbers \(F\) to be the union of the following two sets:

The set \(F\) can be viewed as a subset of \(\mathbb{R}\) with the mapping \(\phi : (m, e) \mapsto mb^e\) and we will make this abuse of notation in what follows. Usually the sign of the floatting point number is separated from \(m\) but we include it "inside" \(m\) as it does not change the proofs below and simplifies the notations.

Let \(a_i \in F\) for \(i = 1,2\) such that \(a_i = m_i b^{e_i}\).

Proposition: \(\phi\) is injective.

Proof: Suppose that \(a_1 = a_2\) or \(m_1b^{e_1} = m_2b^{e_2}\). If \(a_1\) and \(a_2\) are subnormal numbers then \(e_1 = e_2 = -M\) and \(m_1 = m_2\). If \(a_1\) and \(a_2\) are normal numbers suppose that \(e_2 > e_1\), then \(|\frac{m_2b^{e_2}}{m_1b^{e_1}}| > b^{e_2 + p - 1 - e_1 - p} = b^{e_2 - e_1 - 1} \geq b^{1 - 1} = 1\) therefore \(m_2b^{e_2} \neq m_1b^{e_1}\) which is absurd hence \(e_1 = e_2\) and as a consequence \(m_1 = m_2\).

Definition: We define the distance in ULPs between \(a_1\) and \(a_2\) denoted by \(U(a_1, a_2)\) to be:

Example: Take \(a_1 = 123456 \times 10^5\) and \(a_2 = 123789 \times 10^5\) Then as the exponents of \(a_1\) and \(a_2\) are the same we have \(U(123456 \times 10^5, 123789 \times 10^5) = |123789 - 123456| = 333\).

The following proposition confort the name "units in the last place".

Proposition: Let \(f = \lfloor \log_b U(a_1, a_2) \rfloor + 1\) and suppose that \(a_1, a_2\) are of same sign and have the same exponents, then either the first \(p - f\) digits of \(m_1\) and \(m_2\) are identical or their difference is \(\pm 1\).

Proof: For \(i = 1,2\) there exists \(q_i \in \mathbb{Z}\) and \(0 \leq r_i < b^f\) such that \(m_i = q_i b^f + r_i\). Then \(|q_1 - q_2| \leq \frac{|m_1 - m_2| + |r_1 - r_2|}{b^f} < \frac{b^{\log_b(U(a_1, a_2)} + b^f}{b^f} = 2\)

So that either \(q_1 = q_2\) or \(q_1 - q_2 = \pm 1\). It is interesting to know what are the cases when \(q_1 - q_2 \pm 1\). Suppose that \(0 \leq m_1 < m_2\) and that \(q_1 = q_2 + 1\) then \(m_1 = q_1 b^f + r_1 \geq q_2 b^f + b^f > q_2 b^f + r_2 = m_2\) which contradicts the hypothesis hence \(q_1 \leq q_2\). Finally \(r_1 + U(a_1, a_2) = r_1 + (m_2 - m_1) = q_2 b^f + r_2 - q_1 b^f = r_2 + b_f\) so that:

Example: Taking back \(a_1 = 123456 \times 10^5\) and \(a_2 = 123789 \times 10^5\). As \(q_1 = q_2\) we have the first 3 digits of \(a_1\) and \(a_2\) that are identical and they differ by their last \(\log_{10} \lfloor U(a_1, a_2) \rfloor + 1 = \lfloor \log_{10}(333) \rfloor + 1 = 3\)

Example: Now take \(a_1 = 899900 \times 10^5\) and \(a_2 = 900100 \times 10^5\). We have \(f = 3\) but \(q_2 = q_1 + 1\) and \(r_2 = 900 > 100 = r_1\) and \(r_2 + U(a_1, a_2) = 1100 \geq 1000 = 10^3\).

The propositions above show that our definition of the ULP distance is well choosen as we have the following results:

We show now how to compute it using the IEEE 754 floatting point numbers representation. A floatting point number \((m, e) \in F\) is stored in memory (and registers) as the integer \(\pm ((e + M)b^p + |m|)\).

Proposition: If \(e_2 \geq e_1 + 2\) then \(U(a_1, a_2) \geq b^p\).

Proof: We have \(U(a_1, a_2) = |m_2 b^{e_2 - e_1} - m_1| \geq ||m_2| b^{e_2 - e_1} - |m_1||\). But \(m_2\) is a normal number otherwise we would have \(e_2 = -M = e_1\) so that \(|m_2| \geq b^{p - 1}\) and we have \(|m_2| b^{e_2 - e_1} \geq b^{p - 1 + e_2 - e_1} \geq b^{p + 1} > |m_1|\), therefore \(||m_2| b^{e_2 - e_1} - |m_1|| \geq |m_2|b^2 - |m_1| > b^{p - 1 + 2} - b^p = b^p\).

The proposition above basically states that if two floatting point numbers are two orders of magnitude away then that have no digits in common, and that there are godd chances that comparing them is not interesting at all.

The usual definition of the distance in ULPs is roughly given as the number of floatting point numbers between the two considered floatting point numbers. More precisely we will denote it by \(V\) and it is defined as follows:

Proposition: If \(e_1 = e_2\) and \(a_1\), \(a_2\) have the same sign then \(U(a_1, a_2) = V(a_1, a_2)\).

Proof: We have \(V(a_1, a_2) = |(e_1 + M)b^p + m_1 - (e_2 + M)b^p - m_2|\), but as \(e_1 = e_2\), we end up with \(V(a_1, a_2) = |m_1 - m_2| = U(a_1, a_2)\).

Proposition: \(V(a_1, a_2) = 1\) is equivalent to \(U(a_1, a_2) = 1\).

Proof: The proposition is true if \(e_1 = e_2\). Suppose that \(e_2 > e_1\). Note that \(a_2\) is a normal number so that \(m_2 \geq b^{p - 1}\).

We first suppose that \(V(a_1, a_2) = 1\). Then by the definition of \(V\), \(a_1\) and \(a_2\) have same sign otherwise \(V(a_1, a_2) \geq 2\) and we suppose that \(a_i \geq 0\). Moreover we have \(e_2 = e_1 + 1\) otherwise we would have that \(a_1 = m_1b^{e_1} < m_1b^{e_1 + 1} < m_2b^{e_1 + 2} \leq a_2\). Now we have \((b^p - 1)b^{e_1} < b^{p - 1}b^{e_1 + 1}\) and let \((b^p - 1)b^{e_1} \leq mb^e \leq b^{p - 1}b^{e_1 + 1}\).

First note that if \(a = mb^e\) is a normal number then \(m \geq b^{p - 1}\) and if \(a\) is a subnormal number then \(e = -M\) in which case we also have \(e_1 = -M\) and \(m \geq b^p - 1 \geq b^{p - 1}\). In any case \(m \geq b^{p - 1}\).

We have \((b^p - 1)/m b^{e_1} < b^e < b^{p - 1}/m b^{e_1 + 1}\). But \(1 \leq (b^p - 1) / m\) and \(b^{p - 1} / m \leq 1\) so that \(b^{e_1} \leq b^e \leq b^{e_1 + 1}\) and \(e = e_1\) or \(e = e_1 + 1\). In the first case \((b^p - 1)b^{e_1} \leq mb^{e_1}\) so that \(b^p - 1 \leq m\) but \(m < b^p\) and \(m = b^p - 1\). In the second case \(mb^{e_1 + 1} \leq b^{p - 1}b^{e_1 + 1}\) so that \(m \leq b^{p - 1}\) but \(b^{p - 1} \leq m\) and \(m = b^{p - 1}\). We have proven that two consecutive elements of \(F\) with \(e_2 = e_1 + 1\) are neessary of the form \(a_1 = (b^p - 1)b^{e_1}\) and \(a_2 = b^{p - 1}b^{e_1 + 1}\). Now we can compute \(U(a_1, a_2) = |bb^{p - 1} - (b^p - 1)| = 1\).

Conversely, suppose that \(U(a_1, a_2) = 1\), then \(|b^{e_2 - e_1}m_2 - m_1| = 1\). Suppose that \(b^{e_2 - e_1}m_2 - m_1 = -1\), then \(-1 \geq bb^{p - 1} - b^p = 0\) which is absurd. We then have \(b^{e_2 - e_1}m_2 - m_1 = 1\). Suppose that \(e_2 \geq e_1 + 2\) then we would have that \(b^{e_2 - e_1}m_2 - m_1 \geq b^2b^{p - 1} - b^p \geq b^p\) which is absurd so that \(e_2 = e_1 + 1\) and \(bm_2 - m_1 = 1\). Suppose that \(m_2 \geq b^{p - 1} + 1\) then \(bm_2 - m_1 \geq b^p + b - (b^p - 1) \geq 2\) which is absurd so that \(m_2 = b^{p - 1}\) and as a consequence \(m_1 = b^p - 1\).

If \(a_1, a_2 < 0\), then \(V(a_1, a_2) = 1\) is equivalent by definition to \(V(-a_1, -a_2) = 1\) which is equivalent to \(U(-a_1, -a_2) = 1\) which is by definition equivalent to \(U(a_1, a_2) = 1\).

Proposition: Suppose that \(e_1 \leq e_2 \leq e_1 + 1\) then \(V \leq U \leq bV\).

Proof: The proposition is true if \(e_1 = e_2\). Suppose now that \(e_2 = e_1 + 1\). Then we have \(b^p + m_2 - m_1 \geq b^p + b^{p - 1} - b^p \geq 0\) so that \(V(a_1, a_2) = b^p + m_2 - m_1 = b^p + m_2(1 - b) + bm_2 - m_1\). But \(b^p + m_2(1 - b) \leq b^p + b^p(1 - b) \leq 0\) and \(bm_2 - m_1 \geq bb^{p - 1} - b^p = 0\) so that \(V(a_1, a_2) \leq bm_2 - m_1 = U(a_1, a_2)\). On the other hand we have \(bm_2 - m_1 \leq b(b^p + m_2 - m_1 + m_1 - m_1/b - b^p)\) but \(m_1 - m_1/b - b^p \leq b^p - b^{p - 1}/b - b^p \leq 0\) so that \(U(a_1, a_2) \leq b(b^p + m_2 - m_1) = bV(a_1, a_2)\).

Remark: The previous propositions shows that the difference between \(V\) and \(U\) is only visible when the arguments have differents exponents and are non consecutive. Our version of the distance in ULPs puts more weights when crossing powers of \(b\). Also if \(e_2 \geq e_1 + 2\) then we have seen that \(a_1\) and \(a_2\) have nothing in common which is indicated by the fact that \(U, V \geq b^p\).

Definition: We now define the relative distance \(D(a_1, a_2)\) between \(a_1\) and \(a_2\) to be \(|a_1 - a_2| / \min(|a_1|, |a_2|)\).

Proposition: As \(U\) is defined in a "mathematical" way compared to \(V\) then the relation between \(U\) and \(D\) is straightforward and we have \(D(a_1, a_2) = U(a_1, a_2) / |m_1|\). Moreover we have \(b^{-q}U \leq D \leq b^{1 - q}U\) where \(q\) is the greatest integer such that \(b^{q - 1} \leq |m_1| < b^q\). In particular if \(a_1\) is a normal number then \(p = q\).

Proof: Suppose that \(|a_1| < |a_2|\), then we have three cases:

In any case we have \(e_1 \leq e_2\), as a consequence we have \(D(a_1, a_2) = |m_1b^{e_1} - m_2b^{e_2}| / \min(|m_1|b^{e_1}, |m_2|b^{e_2}) = |m_1 - m_2b^{e_2 - e_1}| / \min(|m_1|, |m_2|b^{e_2 - e_1})\). Therefore \(D(a_1, a_2) = U(a_1, a_2) / \min(|m_1|, |m_2|b^{e_2 - e_1})\). Now if \(e_1 = e_2\) then \(\min(|m_1|, |m_2|) = |m_1|\) but if \(e_2 > e_1\) then \(a_2\) is a normal number and \(|m_1| < b^p = b \times b^{p - 1} \leq b^{e_2 - e_1} |m_2|\) and again \(\min(|m_1|, |m_2|b^{e_2 - e_1}) = |m_1|\).

Applying \(b^{q - 1} \leq |m_1| < b^q\) we get that \(b^{-q}U \leq D \leq b^{1 - q}U\). If moreover \(a_1\) is a normal number then by definition \(p = q\).

Remark: Using the inequality of the previous proposition and taking the base-\(b\) logarithm we get \(-q + \log U \leq \log D \leq 1 - q + \log U\) and then \(-q + \lfloor \log U \rfloor \leq \lfloor \log D \rfloor \leq 1 - q + \lfloor \log U \rfloor\) hence two possibilities:

According to a above proposition we know that \(f = 1 + \lfloor \log U \rfloor\) can be interpreted as the number of differents digits in the last places of the mantissa. Write \(\mathcal{D} = - \lfloor \log D \rfloor\) then \(q \leq f + \mathcal{D} \leq q + 1\). The latter inequality shows that \(\mathcal{D}\) can be interpreted as the number of digits which are the same in the mantissa near the "first" place. Note that for denormal numbers the "first" places are near the bit of most significance. We can conclude this remark with the interpretation that two floatting point numbers have at least \(\mathcal{D} - 1\) digits in common in the first place of the mantissa and \(f\) digits which are different in the last place of the mantissa.

Algorithm: We give below the C code for \(U\) with a caveat. As seen in a previous proposition when \(e_2 \geq e_1 + 2\) the arguments have no digit in common and can be considered too far away in which case we return INT_MAX (or LONG_MAX). As a side effect is that the code will be free of multiprecision integers (which would be necessary as soon as \(|e_2 - e_1| \geq 12\)) hence lesser dependencies, readability, maintainability and performances. When \(|e_2 - e_1| \leq 1\) we use the formula of the definition.

/* We suppose that floats are IEEE754 and not NaN nor infinity */

struct fl_t{
  int mantissa;
  int exponent;
};

fl_t decompose(float a_) {
  fl_t ret;
  unsigned int a;
  memcpy(&a, &a_, sizeof(float)); /* avoid aliasing */
  ret.exponent = (int)((a >> 23) & 0xff) - 127;
  if (ret.exponent == -127) {
    /* denormal number */
    ret.mantissa = (int)(a & 0x007fffff);
  } else {
    ret.mantissa = (int)((1 << 23) | (a & 0x007fffff));
  }
  if (a >> 31) {
    ret.mantissa = -ret.mantissa;
  }
  return ret;
}

int distance_ulps(float a_, float b_) {
  fl_t a, b;
  a = decompose(a_);
  b = decompose(b_);

  if (a.exponent - b.exponent < -1 || a.exponent - b.exponent > 1) {
    return INT_MAX;
  }
  
  int d;
  if (a.exponent == b.exponent) {
    d = a.mantissa = b.mantissa;
  } else if (a.exponent > b.exponent) {
    d = 2 * a.mantissa - b.mantissa;
  } else {
    d = 2 * b.mantissa - a.mantissa;
  }

  return d > 0 ? d : -d;
}

The algorithm for computing \(\mathcal{D} - 1\) follows:

int d(float a_, float b_) {
  float absa = fabsf(a_);
  float absb = fabsf(b_);

  /* ensure that |a_| <= |b_| */
  if (absb < absa) {
    float tmp = absa;
    absa = absb;
    absb = tmp;
  }

  fl_t a = decompose(absa);
  int q = 0;
  for (q = 0; q <= 23 && (2 << q) <= a.mantissa; q++);

  int ulps = distance_ulps(a_, b_);
  int lu;
  for (lu = 0; lu <= 30 && (2 << (lu + 1)) <= a.mantissa; lu++);

  return q - (lu + 1) - 1;
}

What we really do in the tests

As said above buggy intrinsics can be easily found. But the bugs appears for corner cases typically involving NaNs and/or infinities. But according to the philosophy of NSIMD, it is not the job of its standard operators to propose a non buggy alternative to a buggy intrinsics. But we still have the problem of testing. A consequence of the philosophy of NSIMD is that we only have to test that intrinsics are correctly wrapped. We can reasonably assume that testing for floatting point numbers on only normal numbers is more than sufficient.

Moreover, an implementation (buggy or not), may have different parameters set that controls how floatting point arithmetic is done on various components of the chip. An non exhaustive list includes:

As a consequence we do not compare floats using the operator = nor do we use a weird-buggy formula involving the machine epsilon. Instead we use the algorithm above to make sure that the first bits are correct. More precisely we use the following algorithm and its variants for float16 and doubles where ufp stands for units in the first place.

/* a_ and b_ must be IEEE754 and normal numbers */
int ufps(float a_, float b_) {
  unsigned int a, b;
  memcpy(&a, &a_, 4);
  memcpy(&b, &b_, 4);
  int ea = (int)((a >> 23) & 0xff);
  int eb = (int)((b >> 23) & 0xff);
  if (ea - eb > 1 || ea - eb < -1) {
    return 0;
  }
  int ma = (int)(a & 0x007fffff);
  int mb = (int)(b & 0x007fffff);
  int d = 0;
  if (ea == eb) {
    d = ma - mb;
  } else if (ea > eb) {
    d = 2 * ma - mb;
  } else {
    d = 2 * mb - ma);
  }
  d = (d >= 0 ? d : -d);
  int i = 0;
  for (; i < 30 && d >= (1 << i); i++);
  return 23 - i;
}