jeudi 7 septembre 2017

How does a C++ compiler choose between deferred and async execution for std::async?

It seems that the default behaviour of std::async heavily favours std::launch::deferred. Consider this code:

#include <iostream>
#include <future>
#include <thread>
#include <chrono>
#include <vector>


int main(void)
{
    std::vector<std::future<void>> task_list;

    size_t n_tasks = 10; // Let's say this could change at runtime
    // The first two seem interchangeable in this example:
    //auto launch_pol = std::launch::deferred;
    //auto launch_pol = std::launch::async | std::launch::deferred;
    // Only this seems to actually do async tasks:
    auto launch_pol = std::launch::async;

    auto start_time = std::chrono::steady_clock::now();

    // Generate a bunch of tasks
    for (size_t i = 0; i < n_tasks; i++) {
        task_list.emplace_back(std::async(launch_pol,
            [i](){
                std::cout << " Starting task " << i << std::endl;
                std::this_thread::sleep_for(std::chrono::seconds(1));
                std::cout << " Stopping task " << i << std::endl;
            }
        ));
        // The following lines are experiments I tried to nudge the
        // task to start doing something.
        if (!task_list.at(i).valid()) {
            std::cout << "Task not valid!" << std::endl;
        }
        task_list.at(i).wait_for(std::chrono::milliseconds(1));
    }


    // Wait for them to complete
    for (auto& task : task_list) {
        task.get();
    }

    std::chrono::duration<double> stop_time =
        std::chrono::steady_clock::now() - start_time;
    std::cout << "Execution time: " << stop_time.count() << std::endl;

    return 0;
}

Notice that I've been experimenting with multiple launch policies. It seems that unless I explicitly state std::launch::async (only!), the compiler will fall back to std::launch::deferred. I tried this with Clang 3.8, gcc 5.4, and this post seems to indicate that MSVC works in the same way.

OK, this is not in contradiction with the C++ standard. If we specify deferred, we may get lazy evaluation, which is (in this case) pretty much the same as a serial execution of my tasks. However, what's the point if the compiler just falls back to std::launch::deferred?

If the compiler is always falling back to lazy evaluation, then calling std::async without std::launch::async seems pointless. I was hoping that the C++ runtime is smart about launching threads (or not) if I use the default launch policy.

Some background: In the problem I'm trying to solve, I'm running a variable number of initialization calls, which are pretty slow, but completely I/O bound (i.e., they wait most of the time for results from elsewhere). The number of these might scale, so I was hoping from some help from the compiler to schedule threads.

Aucun commentaire:

Enregistrer un commentaire