Почему один поток быстрее, чем несколько, даже если они по сути имеют одинаковые издержки?

Я использую 64-битную Windows 7 на 8-ядерном процессоре. Я запустил следующее:

    #include "stdafx.h"#include <iostream>
#include <Windows.h>
#include <process.h>
#include <ctime>

using namespace std;

int count = 0;
int t = time(NULL);

//poop() loops incrementing count until it is 300 million.
void poop(void* params) {
while(count < 300000000) {
count++;
}cout<< time(NULL) - t <<" \n";
}

int _tmain(int argc, _TCHAR* argv[])
{
//_beginthread(poop, 0, NULL);
//_beginthread(poop, 0, NULL);
poop(NULL);

cout<<"done"<<endl;

while(1);

return 0;
}

Я сравнил результат с тем, когда я раскомментировал beginThread’s. Оказывается, однопотоковая версия выполняет это быстрее всего! На самом деле, добавление большего количества потоков делает процесс еще дольше. При подсчете 300 миллионов процесс занял более 8 секунд, что, как я понял, было достаточно, чтобы исключить вызовы функций beginThread + другие незначительные накладные расходы.

Я провел небольшое исследование, и общий вывод о том, что многопоточный процесс должен быть медленнее, это накладные расходы. Но в этом случае, независимо от того, запускаю ли я несколько потоков или один, количество обращений к переменной count (которая существует в сегменте данных, потому что это предварительно выделенная переменная afaik) одинаково. Таким образом, в основном, издержки (если это проблема накладных расходов) не связаны с тем, что доступ к глобальной переменной стоит дороже, чем к локальной переменной.

Глядя на мой диспетчер задач, процесс с одним потоком использует 13% ЦП (около 1/8 ядра), а добавление потоков увеличивает использование ЦП с шагом примерно 1/8. Таким образом, с точки зрения мощности процессора, при условии, что диспетчер задач точно отображает это, добавление потоков использует больше процессора. Что еще больше смущает меня … как получается, что я использую более общий процессор с отдельными ядрами, но в целом для выполнения задачи требуется больше времени?

TLDR: ПОЧЕМУ ЭТО ПРОИСХОДИТ?

5

Решение

Ваш код по сути неверен.

count++ это трехэтапная операция, которая считывает значение, увеличивает его, а затем сохраняет обратно в переменную.
Если два потока работают count++ сразу на одну и ту же переменную, один из них перезапишет изменения другого.

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

Если вы делаете count локальная переменная, время должно выглядеть намного более нормальным.

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

5

Другие решения

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

void poop(void* params) {
int count  = 0;
while(count < 300000000) {
count++;
}
cout<< time(NULL) - t <<" \n";
}

Обратите внимание, что это не проблема для T потому что это только прочитано, не написано, потоками. Однако это проблема с соиЬ как вы также пишете на это из нескольких потоков.

Кроме того, как указано в комментариях, все ваши темы имеют доступ к одной ячейке памяти. Это означает, что когда поток обновляется подсчитывать строка кэша, в которой она хранится, должна быть очищена и перезагружена. Это очень неэффективный доступ к памяти. Обычно это происходит, когда вы обращаетесь к последовательным элементам в массиве, а не к одной переменной (плохая идея, см. Выше). Решением этой проблемы было бы заполнение массива, чтобы убедиться, что каждая запись точно кратна размеру вашей строки кэша L1, что, очевидно, в некоторой степени зависит от вашего целевого процессора. Другим вариантом было бы реструктурировать ваш алгоритм так, чтобы либо; каждый поток обрабатывал большой блок последовательных элементов, или каждый поток обращался к элементам таким образом, чтобы поток не обращался к соседним местоположениям.

Поскольку вы используете Windows, вы можете захотеть использовать более высокий уровень абстракции для своего кода, а не функции потоков Win32. Библиотека параллельных паттернов отвечает всем требованиям здесь (как и Резьбовые строительные блоки Intel).

    concurrency::parallel_invoke(
[=] { poop(nullptr); },
[=] { poop(nullptr); }
);

Это позволяет PPL планировать ваши задачи для пула потоков, а не для того, чтобы ваше приложение явно создавало потоки.

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

3

По вопросам рекламы [email protected]