Я думаю, что большая часть вашей текущей проблемы заключается в том, что вы принимаете ту часть, которая действительно может работать с многопоточными (нахождение простых чисел) и похоронить ее в шуме (время для вывода вывода на консоль).
Чтобы понять, как много эффекта это имеет, я переписал ваш основной немного, чтобы отделить печать простых чисел от поиска простых чисел. Для того, чтобы сделать синхронизации легче, я бы это взять предел из командной строки, а не в интерактивном режиме, давая это:
int main(int argc, char **argv) {
if (argc != 2) {
std::cerr << "Usage: bad_prime <limit:long>\n";
return 1;
}
std::vector<unsigned long> primes;
unsigned long lim = atol(argv[1]);
clock_t start = clock();
for(unsigned long i = 1; i <= lim; i++)
if(isprime(i))
primes.push_back(i);
clock_t stop = clock();
for (auto a : primes)
std::cout << a << "\t";
std::err << "\nTime to find primes: " << double(stop-start)/CLOCKS_PER_SEC << "\n";
}
Пропуская тысячи строк простых чисел себя, я получаю результат, как это:
Time to find primes: 0.588
Real 48.206
User 1.68481
Sys 3.40082
Итак - примерно полсекунды, чтобы найти простые числа и более 47 секунд для их печати. Предполагая, что намерение состоит в том, чтобы написать вывод на консоль, мы могли бы также остановиться прямо там. Даже если многопоточность может полностью устранить время, чтобы найти простые числа, мы все равно только изменим предельное время от ~ 48,2 секунд до ~ 47,6 секунд - вряд ли стоит того.
В настоящее время я предполагаю, что реальное намерение состоит в том, чтобы написать результат на что-то вроде файла. Поскольку кажется довольно бессмысленным идти на работу по созданию многопоточного кода, но запускать ужасно неэффективный код в каждом потоке, я думал, что буду оптимизировать (или, по крайней мере, де-пессимизировать) однопоточный код как стартовый точка.
Во-первых, я удалил endl
и заменил его на "\n"
. С выходом, направленным к файлу, это сократило время выполнения от 0,968 секунд до 0,678 секунды - endl
сбрасывает буфер в дополнение к написанию новой строки, и эта промывка буфера составляет примерно одну треть времени, затраченного программой в целом.
На том же основании, я взял на себя смелость переписывать isprime
на то, что по крайней мере немного меньше неэффективно:
bool isprime(unsigned long num) {
if (num == 2)
return true;
if(num == 1 || num % 2 == 0)
return false;
unsigned long lim = sqrt(num);
for(unsigned long i = 3; i <= lim; i+=2)
if (num % i == 0)
return false;
return true;
}
Это, конечно, открыто для дополнительного улучшения (например, решето Эратосфена), но это просто, прямо и примерно в два-три раза быстрее (времена выше основаны на использовании этого isprime
, а не вашего).
На данный момент многопоточность первичной находки, по крайней мере, дает шанс сделать какой-то смысл: с первичным нахождением, взяв приблизительно 0,5 из 0,6 секунды, даже если мы можем только удвоить скорость, мы должны увидеть значительное разница в общем времени.
Отделив вывод от первичного поиска, также дает нам гораздо лучший базис для написания многопоточной версии кода. Когда каждый поток записывает свои результаты в отдельный вектор, мы можем получить значимый (не чередующийся) вывод без необходимости блокировки на cout
и таких - мы вычисляем каждый кусок отдельно, а затем распечатываем каждый вектор по порядку.
Код для этого может выглядеть примерно так:
#include <iostream>
#include <vector>
#include <time.h>
#include <math.h>
#include <thread>
using namespace std;
bool isprime(unsigned long num) {
// same as above
}
typedef unsigned long UL;
struct params {
unsigned long lower_lim;
unsigned long upper_lim;
std::vector<unsigned long> results;
params(UL l, UL u) : lower_lim(l), upper_lim(u) {}
};
long thread_func(params *p) {
for (unsigned long i=p->lower_lim; i<p->upper_lim; i++)
if (isprime(i))
p->results.push_back(i);
return 0;
}
int main(int argc, char **argv) {
if (argc != 2) {
std::cerr << "Usage: bad_prime <limit:long>\n";
return 1;
}
unsigned long lim = atol(argv[1]);
params p[] = {
params(1, lim/4),
params(lim/4, lim/2),
params(lim/2, 3*lim/4),
params(3*lim/4, lim)
};
std::thread threads[] = {
std::thread(thread_func, p),
std::thread(thread_func, p+1),
std::thread(thread_func, p+2),
std::thread(thread_func, p+3)
};
for (int i=0; i<4; i++) {
threads[i].join();
for (UL p : p[i].results)
std::cout << p << "\n";
}
}
Запуск этого на той же машине, как и раньше (довольно старый двухъядерный процессор), я получаю:
Real 0.35
User 0.639604
Sys 0
Это кажется для масштабирования чрезвычайно хорошо. Если бы все, что мы получили, было многоядерным вычислением, мы ожидаем увидеть время, чтобы разделить простые числа на 2 (я запускаю это на двухъядерном процессоре), и время записи данных на диск остается постоянным (многопоточность не ускорит мой жесткий диск). Исходя из этого, идеальное масштабирование должно дать нам 0,59/2 + 0,1 = 0,40 секунды.
(по общему признанию) незначительное улучшение, которое мы видим за пределами этого, скорее всего, связано с тем фактом, что мы можем начать запись данных из потока 1 на диск, в то время как потоки 2, 3 и 4 все еще находят простые числа (и аналогично , начните записывать данные из потока 2, пока 3 и 4 все еще вычисляют и записывают данные из потока 3, пока поток 4 все еще вычисляет).
Я полагаю, что я должен добавить, что улучшения, которые мы видим, достаточно малы, чтобы также быть простым шумом в сроках. Тем не менее, я выполнял как одно-, так и многопоточные версии, и, несмотря на некоторые различия в обеих версиях, многопоточная версия постоянно быстрее, чем должно учитываться улучшение скорости вычислений.
Я почти забыл: чтобы получить представление о том, насколько сильно это происходит в общей скорости, я проверил тест, чтобы узнать, сколько времени потребуется, чтобы найти простые цифры до 13 633 943, которые вы нашли в первоначальной версии за одну минуту. Несмотря на то, что я почти наверняка использую более медленный процессор (7-летний Athlon 64 X2 5200+), эта версия кода делает это за 12,7 секунды.
Последнее замечание: по крайней мере, на данный момент я не добавил заполнение, которое вы вставили, чтобы предотвратить ложное разделение. В зависимости от времени, которое я получаю, они не кажутся необходимыми (или полезными).
может быть медленным из-за времени переключения контекста потока –
FYI: 'lim = sqrt (num)' будет значительно быстрее для больших значений. –
Кроме того, 'iter2' и' iter4' имеют почти ничего общего. –