Подтвердить что ты не робот

С++ 11 vs async performance (VS2013)

Я чувствую, что здесь что-то не хватает...

Я немного изменил код, чтобы перейти от использования std::thread к std::async и заметил существенное увеличение производительности. Я написал простой тест, который, как я полагаю, должен выполняться почти одинаково, используя std::thread, поскольку он использует std::async.

std::atomic<int> someCount = 0;
const int THREADS = 200;
std::vector<std::thread> threadVec(THREADS);
std::vector<std::future<void>> futureVec(THREADS);
auto lam = [&]()
{
    for (int i = 0; i < 100; ++i)
        someCount++;
};

for (int i = 0; i < THREADS; ++i)
    threadVec[i] = std::thread(lam);
for (int i = 0; i < THREADS; ++i)
    threadVec[i].join();

for (int i = 0; i < THREADS; ++i)
    futureVec[i] = std::async(std::launch::async, lam);
for (int i = 0; i < THREADS; ++i)
    futureVec[i].get();

Я не слишком углублялся в анализ, но некоторые предварительные результаты показали, что код std::async работает примерно в 10 раз быстрее! Результаты немного менялись при оптимизации, я также попытался переключить порядок выполнения.

Является ли это проблемой компилятора Visual Studio? Или есть какая-то более глубокая проблема с реализацией, которую я пропускаю, учитывая эту разницу в производительности? Я думал, что std::async является оберткой вокруг вызовов std::thread?


Также учитывая эти различия, мне интересно, какой способ получить лучшую производительность здесь? (Есть больше, чем std:: thread и std:: async, которые создают потоки)

А что, если мне нужны отдельные потоки? (std:: async не может это сделать, насколько мне известно)

4b9b3361

Ответ 1

Когда вы используете async, вы не создаете новые потоки, вместо этого вы повторно используете те, которые доступны в пуле потоков. Создание и уничтожение потоков - очень дорогостоящая операция, требующая около 200 000 циклов ЦП в ОС Windows. Кроме того, помните, что наличие нескольких потоков намного больше, чем количество ядер процессора, означает, что операционная система должна тратить больше времени на их создание и планировать их использовать доступное время процессора в каждом из ядер.

UPDATE: Чтобы увидеть, что количество потоков, используемых с помощью std::async, намного меньше, чем при использовании std::thread, я изменил код тестирования, чтобы подсчитать количество уникальных идентификаторов потоков, используемых при каждом запуске, как показано ниже. Результаты на моем ПК показывают этот результат:

Number of threads used running std::threads = 200
Number of threads used to run std::async = 4

но число потоков с std::async показывает изменения от 2 до 4 на моем ПК. В основном это означает, что std::async будет повторно использовать потоки вместо создания новых. Любопытно, что если я увеличиваю вычислительное время лямбды, заменив 100 на 1000000 итераций в цикле for, число асинхронных потоков увеличивается до 9, но с использованием необработанных потоков оно всегда дает 200. Стоит иметь в виду, что "один раз поток завершился, значение std:: thread:: id может быть повторно использовано другим потоком"

Вот код тестирования:

#include <atomic>
#include <vector>
#include <future>
#include <thread>
#include <unordered_set>
#include <iostream>

int main()
{
    std::atomic<int> someCount = 0;
    const int THREADS = 200;
    std::vector<std::thread> threadVec(THREADS);
    std::vector<std::future<void>> futureVec(THREADS);

    std::unordered_set<std::thread::id> uniqueThreadIdsAsync;
    std::unordered_set<std::thread::id> uniqueThreadsIdsThreads;
    std::mutex mutex;

    auto lam = [&](bool isAsync)
    {
        for (int i = 0; i < 100; ++i)
            someCount++;

        auto threadId = std::this_thread::get_id();
        if (isAsync)
        {
            std::lock_guard<std::mutex> lg(mutex);
            uniqueThreadIdsAsync.insert(threadId);
        }
        else
        {
            std::lock_guard<std::mutex> lg(mutex);
            uniqueThreadsIdsThreads.insert(threadId);
        }
    };

    for (int i = 0; i < THREADS; ++i)
        threadVec[i] = std::thread(lam, false); 

    for (int i = 0; i < THREADS; ++i)
        threadVec[i].join();
    std::cout << "Number of threads used running std::threads = " << uniqueThreadsIdsThreads.size() << std::endl;

    for (int i = 0; i < THREADS; ++i)
        futureVec[i] = std::async(lam, true);
    for (int i = 0; i < THREADS; ++i)
        futureVec[i].get();
    std::cout << "Number of threads used to run std::async = " << uniqueThreadIdsAsync.size() << std::endl;
}

Ответ 2

Как и все ваши потоки, попробуйте обновить тот же atomic<int> someCount, ухудшение производительности также можно связать с contention (атомный, убедившись, что все совпадающие обращения упорядочивается по порядку). Следствием может быть то, что:

  • потоки тратят свое время на ожидание.
  • но они все равно потребляют циклы процессора
  • поэтому ваша пропускная способность системы будет потрачена впустую.

При async() тогда будет достаточно, чтобы произошли некоторые изменения в планировании, что может привести к значительному сокращению конкуренции и увеличению пропускной способности. Например, в стандарте указано, что объект функции launch::async будет выполняться "как будто в новом потоке выполнения, представленном объектом потока...". Он не говорит, что это должен быть выделенный поток (поэтому он может быть - но не обязательно - пул потоков). Другая гипотеза может заключаться в том, что реализация требует более расслабленного планирования, потому что ничто не говорит о том, что поток необходимо выполнить немедленно (ограничение, однако, заключается в том, что оно выполнялось до get()).

Рекомендация

Контрольный показатель должен быть сделан с разнесением проблем. Поэтому для многопоточной производительности следует избегать межпоточной синхронизации.

Имейте в виду, что если у вас больше, чем thread::hardware_concurrency() потоков, нет истинного concurrency, и ОС должна управлять накладными расходами на переключение контекста.

Изменить: Некоторая экспериментальная обратная связь (2)

С помощью лам-петли 100, результат теста, который я измеряю, не может использоваться из-за величины ошибки, связанной с разрешением тактовых импульсов 15 мс.

Test case            Thread      Async 
   10 000 loop          78          31
1 000 000 loop        2743        2670    (the longer the work, the smaler the difference)
   10 000 + yield()    500        1296    (much more context switches) 

При увеличении числа THREADS время эволюционирует пропорционально, но только для тестовых случаев с короткой работой. Это говорит о том, что наблюдаемая разница фактически связана с служебными данными при создании потоков, а не их плохим исполнением.

Во втором эксперименте я добавил код для подсчета количества потоков, которые действительно задействованы, на основе вектора, хранящего this_thread::get_id(); для каждого выполнения:

  • Для версии нити не удивительно, что всегда создано 200 (здесь).
  • Очень интересно, что версия async() отображает от 8 до 15 процессов в случае более короткой работы, но показывает увеличение количества потоков (до 131 в моих тестах), когда работа становится длиннее.

Это говорит о том, что async не является традиционным пулом потоков (т.е. с ограниченным числом потоков), а скорее повторяет потоки, если они уже завершили работу. Это, конечно, уменьшает накладные расходы, особенно для небольших задач. (Соответственно, я обновил свой первоначальный ответ)