С++ 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 не может это сделать, насколько мне известно)
Ответы
Ответ 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 не является традиционным пулом потоков (т.е. с ограниченным числом потоков), а скорее повторяет потоки, если они уже завершили работу. Это, конечно, уменьшает накладные расходы, особенно для небольших задач. (Соответственно, я обновил свой первоначальный ответ)