unexpected bug in conversion between std::chrono::duration with different Rep

Viewed 41

I noticed unexpected behaviour and narrowed it down to conversion from one duration to another.

#include <chrono>
#include <iostream>
#include <cmath>

using sec = std::chrono::seconds;
using fsec = std::chrono::duration<float>;
using dsec = std::chrono::duration<double>;

template<typename T>
auto count_intervals(T time_jump) -> uint32_t
{
  constexpr auto interval = sec(10);
  std::cout << "[debug] " << time_jump.count() << std::endl;
  std::cout << "[debug] " << interval.count() << std::endl;
  std::cout << "[debug] " << time_jump.count() / interval.count() << std::endl;
  printf("[debug] %.10f\n", time_jump.count());
  printf("[debug] %.10f\n", time_jump.count() / interval.count());
  const uint32_t k = std::ceil(time_jump.count() / interval.count());
  std::cout << "[debug] " << k << std::endl;
  return k;
}

int main()
{
  using clock_t = std::chrono::system_clock;
  using time_point_t = typename clock_t::time_point;
  using std::chrono::duration_cast;

  const auto start_point = time_point_t();
  const auto a = start_point + sec(25);
  const auto b = start_point + sec(55);

  std::cout << count_intervals<fsec>(b - a) << std::endl;
  std::cout << count_intervals<fsec>(duration_cast<fsec>(b - a)) << std::endl;
  const auto distance = duration_cast<std::chrono::duration<uint32_t>>(b - a);
  std::cout << count_intervals<fsec>(distance) << std::endl;
  std::cout << count_intervals<dsec>(b - a) << std::endl;
}

I cannot figure out why std::ceil rounds up. It seems when I use dsec instead of fsec all is fine. The question is what is my mistake in case of float duration representation. I thought I use std::chrono::duration as intended but obviously that is not the case. How should I perform conversion to fsec correctly? In general I would like to have a fraction-of-a-second duration format.

output

[debug] 30
[debug] 10
[debug] 3
[debug] 30.0000019073
[debug] 3.0000002384
[debug] 4
4
[debug] 30
[debug] 10
[debug] 3
[debug] 30.0000019073
[debug] 3.0000002384
[debug] 4
4
[debug] 30
[debug] 10
[debug] 3
[debug] 30.0000000000
[debug] 3.0000000000
[debug] 3
3
[debug] 30
[debug] 10
[debug] 3
[debug] 30.0000000000
[debug] 3.0000000000
[debug] 3
3
1 Answers

On your platform, system_clock::time_point is a type alias for time_point<system_clock, nanoseconds>. It turns out that this detail is important.

When running the float version of this test, the very first step is convert b - a to duration<float> to make the call to count_intervals. The type of b - a is nanoseconds. The value of b - a is 30'000'000'000ns.

It takes 25 bits of precision to store the integer 30'000'000'000. IEEE float has 24 bits of precision.

The first step in converting 30'000'000'000ns to duration<float> is store the number 30'000'000'000 into the common_type of system_clock::rep and float which is float. It can only do this approximately. It actually stores 30'000'001'024, which is the best representable value under the "round toward nearest, and to even on tie" policy. It then divides this approximation by 1'000'000'000 (stored in a float), and gets approximately 30. The exact value it gets is 0x1.e00002p+4, or 30.000001...

From here on out, the computation is slightly off from exact.

You can see this for yourself by setting cout to hexfloat mode which will output floating point numbers exactly:

std::cout << std::hexfloat;

And/or, set precision to 15 (or so):

std::cout.precision(15);

The computation works as you expect with duration<double> because double has 53 bits of precision and can thus store 30'000'000'000 exactly.

Related