Search code examples
c++stlsleep

std::this_thread::sleep_until timing is completely off by about a factor of 2, inexplicably


Ok, I really have no idea why this is happening. I'm currently implementing a thread container which runs an infinite loop in a detached manner, limited to a certain speed between each iteration.

Header:

class timeloop
{
public:
    std::thread thread = { };
    bool state = true;

    void (*function_pointer)() = nullptr;

    double ratio = 1.0f;
    std::chrono::nanoseconds elapsed = { };

    timeloop(
        void (*function_pointer)() = nullptr
    );

    void function();
};

Definition:

void timeloop::start()
{
    this->thread = std::thread(
        &loop::function,
        this
    );
}

void timeloop::function()
{
    std::chrono::steady_clock::time_point next;

    std::chrono::steady_clock::time_point start;
    std::chrono::steady_clock::time_point end;

    while (
        this->state
        )
    {
        start = std::chrono::high_resolution_clock::now();
        next = start + std::chrono::nanoseconds(
            (long long) (this->ratio * (double) std::chrono::nanoseconds::period::den)
        );

        if (
            this->function_pointer != nullptr
            )
        {
            this->function_pointer();
        }

        /***************************
            this is the culprit
        ***************************/
        std::this_thread::sleep_until(
            next
        );

        end = std::chrono::high_resolution_clock::now();
        this->elapsed = std::chrono::duration_cast<std::chrono::nanoseconds>(
            end - start
            );
    }
}

Calling code:

timeloop* thread_draw = new timeloop(
    &some_void_function
);
thread_draw->ratio = 1.0 / 128.0;

thread_draw->start();
thread_draw->thread.detach();

The definition code is behaving weirdly, specifically std::this_thread::sleep_until. With this->ratio = 1.0 / 128.0 I'm expecting a framerate of around 128, the computed values of start and next reinforce this, yet it inexplicably hovers at around 60. And yeah, I tried just dividing next by 2, but that actually made it drop to around 40.

Extra code to verify the normal time to sleep for:

auto diff = std::chrono::nanoseconds(
    next - start
).count() / (double) std::chrono::nanoseconds::period::den;
auto equal = diff == this->ratio;

where equal evaluates to true.

Frame rate calculation:

double time = (double) thread_draw->elapsed.count() / (double) std::chrono::nanoseconds::period::den;
double fps = 1.0 / time;

Though I also used external FPS counters to verify (NVIDIA ShadowPlay and RivaTuner/MSI Afterburner), and they were in a range of about +-5 of the calculated value.

And I know it's std::this_thread::sleep_until because once I comment that out, the frame rate jumps up to around 2000. Yeah...

I'm truly baffled at this, especially seeing how I can't find any evidence of anybody else ever having had this problem. And yes, I'm aware that sleep functions aren't perfectly accurate, and there's bound to be hiccups every now and then, but consistently sleeping for pretty much double the scheduled time is just absurd.

Did I perhaps misconfigure a compiler option or something? It's definitely not a performance problem, and I'm reasonably sure it's not a logic error either (seeing how all the calculations check out) [unless I'm abusing chrono somewhere].


Solution

  • There are no guarantees on resolution of sleep_until, you are only guaranteed the thread will not be woken before the timepoint. If you are implementing the main game loop, read Fix your timestep.

    Using sleep to guarantee timing is a terrible way to do it. You are at mercy of OS scheduler and e.g. Windows has a minimal sleep amount about 10 milliseconds I believe. (If the implementation actually asks the OS to put the thread to sleep and the OS decides to do a context switch.)

    The lag might also be caused by VSync in the drawing thread if you are calling glfwSwapBuffers or similar. That would explain why your are limited to 60FPS, but not why commenting sleep solves the problem.

    So my guess is the OS's sleep above. I would recommend to remove the sleep and rely on VSync, that's the right frequency you want to draw at anyway. Synchronization with logic threads will be a pain in... but that's always the case.