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

Почему 50 потоков быстрее, чем 4?

DWORD WINAPI MyThreadFunction(LPVOID lpParam) {
    volatile auto x = 1;
    for (auto i = 0; i < 800000000 / MAX_THREADS; ++i) {
        x += i / 3;
    }
    return 0;
}

Эта функция запускается в потоках MAX_THREADS.
Я запустил тесты на Intel Core 2 Duo, Windows 7, MS Visual Studio 2012 с помощью Concurrency Visualizer с MAX_THREADS=4 и MAX_THREADS=50.
test1 (4 потока) завершено в 7,1 секунды, но test2 (50 потоков) завершено в 5,8 секунды, тогда как test1 имеет больше контекстных переключателей, чем test2.
Я провел те же тесты на Intel Core i5, Mac OS 10.7.5 и получил те же результаты.

4b9b3361

Ответ 1

Я решил сравнить это на своей 4-ядерной машине. Я напрямую сравнивал 4 потока с 50 потоками, чередуя 100 тестов каждого из них. Я использовал свои собственные номера, чтобы у меня было разумное время выполнения для каждой задачи.

Результат был, как вы описали. 50-потоковая версия немного быстрее. Вот график моих результатов:

Parallel task comparison graph

Почему? Я думаю, что это сводится к планированию потоков. Задача не завершена, пока все потоки не выполнили свою работу, и каждый поток должен выполнить четверть задания. Поскольку ваш процесс используется совместно с другими процессами в системе, если какой-либо отдельный поток переключается на другой процесс, это задержит всю задачу. Пока мы ждем окончания последнего потока, все остальные ядра простаивают. Обратите внимание на то, как распределение по времени теста с 4 потоками намного шире, чем 50-ниточный тест, который мы можем ожидать.

При использовании 50 потоков каждый поток имеет меньше возможностей. Из-за этого любые задержки в одном потоке будут иметь меньшее влияние на общее время. Когда планировщик занят нормированием сердечников множеством коротких потоков, задержка на одном ядре может быть скомпенсирована путем предоставления этим потокам времени на другом ядре. Общий эффект латентности на одном ядре - это не столько шоу-стоппер.

Таким образом, казалось бы, в этом случае дополнительное контекстное переключение не является самым большим фактором. В то время как коэффициент усиления мал, представляется полезным немного заманить планировщик потоков, учитывая, что обработка намного важнее, чем переключение контекста. Как и во всем, вы должны найти правильный баланс для своего приложения.


[edit] Из любопытства я провел тест на ночь, пока мой компьютер не делал ничего другого. На этот раз я использовал 200 образцов за тест. Опять же, тесты были чередующимися, чтобы уменьшить влияние любых локализованных фоновых задач.

Первый график этих результатов - для низкого количества потоков (до 3-кратного числа ядер). Вы можете видеть, как некоторые варианты подсчета потоков довольно плохие... То есть все, что не кратно количеству ядер и особенно нечетным значениям.

Additional test plot - low thread count

Второй график предназначен для увеличения количества потоков (от 3-кратного числа ядер до 60).

Additional test plot - high thread count

Выше вы можете увидеть определенный нисходящий тренд по мере увеличения количества потоков. Вы также можете увидеть, что распространение результатов узкое, поскольку количество потоков увеличивается.

В этом тесте интересно отметить, что производительность 4-поточных и 50-поточных тестов была примерно одинаковой, а распространение результатов в 4-ядерном тесте было не таким большим, как мой первоначальный тест. Поскольку компьютер не делал ничего другого, он мог посвятить время испытаниям. Было бы интересно повторить тест, поставив одно ядро ​​на нагрузку менее 75%.

И просто для того, чтобы держать вещи в перспективе, рассмотрите это:

Scaling threads


[Еще одно редактирование]. После публикации моей последней части результатов я заметил, что график смешавших ящиков показал тенденцию к тем испытаниям, которые были кратными 4, но данных было немного сложно увидеть.

Я решил сделать тест с несколькими кратными четырьмя, и подумал, что могу одновременно найти точку уменьшения прибыли. Таким образом, я использовал количество потоков, которые имеют мощность от 2 до 1024. Я бы поднялся выше, но Windows прослушивает около 1400 потоков.

Результат довольно приятный, я думаю. Если вы задаетесь вопросом, что такое маленькие круги, это медианные значения. Я выбрал его вместо красной строки, которую я использовал ранее, потому что он показывает тренд более четко.

Trend for exponentiating the thread-count

Кажется, что в этом конкретном случае плата за оплату находится где-то между 50 и 150 потоками. После этого преимущество быстро исчезает, и мы входим на территорию чрезмерного управления потоками и переключения контекста.

Результаты могут значительно отличаться с более длинной или более короткой задачей. В этом случае задача была связана с множеством бессмысленной арифметики, которая занимала примерно 18 секунд для вычисления на одном ядре.

При настройке только числа потоков я смог сэкономить от 1,5% до 2% от среднего времени выполнения 4-потоковой версии.

Ответ 2

Все зависит от того, что делают ваши потоки.

Ваш компьютер может одновременно запускать столько потоков, сколько есть ядер в системе. Это включает в себя виртуальные ядра с помощью таких функций, как Hyper-threading.

CPU переплете

Если ваши потоки связаны с ЦП (что означает, что они тратят большую часть своего времени на вычисления в данных, находящихся в памяти), вы увидите небольшое улучшение, увеличив количество потоков выше числа ядер. Фактически вы теряете эффективность при запуске большего количества потоков из-за дополнительных накладных расходов, связанных с контекст-swtich потоками и ядрами ядра.

I/O переплете

Где (#threads > #cores) поможет, когда ваши потоки связаны с I/O, что означает, что они проводят большую часть своего времени, ожидая ввода/вывода (жесткий диск, сеть, другое оборудование и т.д.). В этом случае поток, который заблокирован в ожидании завершения ввода-вывода, будет снят с CPU, и вместо этого будет добавлен поток, который действительно готов что-то сделать.

Способ получить максимальную эффективность - всегда держать процессор занятым потоком, который на самом деле что-то делает. (Не ожидая чего-то, а не переключение контекста на другие потоки.)

Ответ 3

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

Вот код, который я придумал (это в системе Linux, поэтому я использую pthreads, и я удалил "WINDOWS-isms":

#include <iostream>
#include <pthread.h>
#include <cstring>

int MAX_THREADS = 4;

void * MyThreadFunction(void *) {
    volatile auto x = 1;
    for (auto i = 0; i < 800000000 / MAX_THREADS; ++i) {
        x += i / 3;
    }
    return 0;
}


using namespace std;

int main(int argc, char **argv)
{
    for(int i = 1; i < argc; i++)
    {
    if (strcmp(argv[i], "-t") == 0 && argc > i+1)
    {
        i++;
        MAX_THREADS = strtol(argv[i], NULL, 0);
        if (MAX_THREADS == 0)
        {
        cerr << "Hmm, seems like end is not a number..." << endl;
        return 1;
        }       
    }
    }
    cout << "Using " << MAX_THREADS << " threads" << endl;
    pthread_t *thread_id = new pthread_t [MAX_THREADS];
    for(int i = 0; i < MAX_THREADS; i++)
    {
    int rc = pthread_create(&thread_id[i], NULL, MyThreadFunction, NULL);
    if (rc != 0)
    {
        cerr << "Huh? Pthread couldn't be created. rc=" << rc << endl;
    }
    }
    for(int i = 0; i < MAX_THREADS; i++)
    {
        pthread_join(thread_id[i], NULL);
    }
    delete [] thread_id;
}

Запуск этого с множеством потоков:

[email protected] junk]$ g++ -Wall -O3 -o thread_speed thread_speed.cpp -std=c++0x -lpthread
[[email protected] junk]$ time ./thread_speed -t 4
Using 4 threads

real    0m0.448s
user    0m1.673s
sys 0m0.004s
[[email protected] junk]$ time ./thread_speed -t 50
Using 50 threads

real    0m0.438s
user    0m1.683s
sys 0m0.008s
[[email protected] junk]$ time ./thread_speed -t 1
Using 1 threads

real    0m1.666s
user    0m1.658s
sys 0m0.004s
[[email protected] junk]$ time ./thread_speed -t 2
Using 2 threads

real    0m0.847s
user    0m1.670s
sys 0m0.004s
[[email protected] junk]$ time ./thread_speed -t 50
Using 50 threads

real    0m0.434s
user    0m1.670s
sys 0m0.005s

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

Это работает на четырехъядерном процессоре, поэтому вы можете видеть, что времена "более 4 потоков" показывают то же самое "реальное" время, что и "4 потока".

Я очень сомневаюсь, что в Windows с нитями есть что-то другое.

Я также скомпилировал код с #define MAX_THREADS 50 и опять же с 4. Это не имело никакого значения для отправленного кода - но просто для того, чтобы охватить альтернативу, где компилятор оптимизирует код.

Кстати, тот факт, что мой код работает примерно в три-десять раз быстрее, указывает, что исходный код использует режим отладки?

Ответ 4

Недавно я несколько раз тестировал Windows, (Vista 64 Ultimate), на ядре 4/8 i7. Я использовал аналогичный код подсчета, представленный как задачи в threadpool с различным количеством потоков, но всегда с тем же общим объемом работы. Нити в пуле получили низкий приоритет, так что все задачи были поставлены в очередь перед потоками и временем. Очевидно, что поле в противном случае было бездействующим, (~ 1% процессора, используемого для сервисов и т.д.).

8 tests,
400 tasks,
counting to 10000000,
using 8 threads:
Ticks: 2199
Ticks: 2184
Ticks: 2215
Ticks: 2153
Ticks: 2200
Ticks: 2215
Ticks: 2200
Ticks: 2230
Average: 2199 ms

8 tests,
400 tasks,
counting to 10000000,
using 32 threads:
Ticks: 2137
Ticks: 2121
Ticks: 2153
Ticks: 2138
Ticks: 2137
Ticks: 2121
Ticks: 2153
Ticks: 2137
Average: 2137 ms

8 tests,
400 tasks,
counting to 10000000,
using 128 threads:
Ticks: 2168
Ticks: 2106
Ticks: 2184
Ticks: 2106
Ticks: 2137
Ticks: 2122
Ticks: 2106
Ticks: 2137
Average: 2133 ms

8 tests,
400 tasks,
counting to 10000000,
using 400 threads:
Ticks: 2137
Ticks: 2153
Ticks: 2059
Ticks: 2153
Ticks: 2168
Ticks: 2122
Ticks: 2168
Ticks: 2138
Average: 2137 ms

С задачами, которые занимают много времени, и с очень небольшим количеством кэша для изменения контекста, количество используемых потоков практически не влияет на общее время выполнения.

Ответ 5

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

Рассмотрим ситуацию, когда ваш поток 4 потоков выполняется на 4 ядрах, и из-за конфигурации загрузки системы одному из ядер удается закончить на 50% быстрее, чем другие: за оставшееся время процесса ваш процессор сможет выделить 3/4 своей мощности обработки для вашего процесса, так как осталось только 3 потока. В том же сценарии загрузки процессора, но со многими другими потоками, рабочая нагрузка разделяется на многие другие подзадачи, которые могут распределяться более тонко между ядрами, при прочих равных условиях (*).

В этом примере показано, что разница во времени не зависит от количества потоков, а скорее от того, как работа была разделена, что намного более устойчиво к неравномерной доступности ядер в последнем случае. Та же самая программа, построенная только с 4 потоками, но где работа абстрагируется с серией небольших задач, выполняемых потоками, как только они будут доступны, в среднем привела бы к получению аналогичных или даже лучших результатов, хотя бы накладные расходы на управление очередь задач.

Более тонкая гранулярность набора задач процесса дает ему лучшую гибкость.


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