LINUX.ORG.RU

Мне всегда казалось, что несколько потоков должны грузить все ядра процессора...

 , , ,


1

5

Всем привет.

UPD2: добавлены реализации boost и OpenMP, почищен код, добавлены комменты в код, немного переформулировано ТЗ (суть не поменялась).

Признаюсь: первый опыт с многопоточной программой, так что прошу ногами сильно не бить.

Решил написать простую программу генерации простых чисел по методу вычеркивания. В развитии этой темы, написал 6 алгоритмов, 4 из которых многопоточные, близнецы, но используют std:thread (метод Initiate3), std:async (Initiate4), boost (Initiate5), OpenMP (Initiate6). Изначально ожидал, что многопоточные алгоритмы загрузят все ядра процессора на 100% и дадут увеличение скорости работы программы. Результаты удивили:
- Все реализации выполняются примерно за одно время (даже OpenMP).
- Многопоточные реализации не быстрее однопоточной, а иногда даже медленнее!!!
std:thread - грузит одно ядро на 100%
std:async - грузит два ядра, общая нагрузка не более 100% одного ядра.
boost - грузит одно ядро
OpenMP - грузит два ядра (суммарная нагрузка 200%), но при этом выполняется не быстрее (обогреватель воздуха помещения)!

Вопрос 1: почему Initiate3,4,5 не грузит все ядра на полную мощность?
Вопрос 2: почему Initiate6 (OpneMP) грузит все ядра, но пи этом не быстрее?
Вопрос 3: Как получить профит от многопоточности в данной конкретной задаче?

Для тех, кто решится помочь - заранее спасибо, и вот инфа в помощь:

Краткое представление алгоритма. Создается массив, элементы которого соответствует числам 3, 5, 7... (ведь четные числа не простые априори). Если соотв. число простое, то значение элемента массива 1, если не простое - 0 (для чистоты эксперимента специально заменил bool на unsigned char). Заполняет массив функция Initiate, в параметрах которой - максимальное число до которого искать. Initiate1 втупую проверяет каждое число, Initiate2 - при нахождении простого числа проходит массив вперед и убирает (помечает не-простыми) соотв. числа, Initiate3,4,5,6 - то же, но с многопоточностью. MarkNonPrime2 и MarkNonPrime3 - близнецы, но вторая предназначена для запуска в отдельном потоке. Остальное, думаю, будет понятно из кода.

Запускать так:

$ g++ -std=c++11 -lpthread -L/usr/lib -lboost_thread prime.cpp -fopenmp -O0 -o prime && time ./prime

Выбор реализации - в main() вызвать нужный метод InitiateX.

Длительность выполнения программы регулировать изменяя параметр InitiateX в main.

Собственно код (prime.cpp):

#include <iostream>
#include <iomanip>
#include <vector>

#include <thread>

#include <future>

#include <boost/thread/thread.hpp>

using namespace std;

// Maximum threads that could be spawned (except OpenMP)
#define MAX_THREAD 100

class CPrimeNumbers{
public:
  vector<unsigned char> numbers; // Array starts with 3 and has a step of 2: 3, 5, 7, 9 ...
  
  inline uint NumberToIndex(const uint number) {return (number-3)/2;}; // BUG: check whether number is < 3
  inline uint IndexToNumber(const uint index) {return index*2+3;};
  
  void MarkNonPrime2(const uint v,const uint max); // Single thread version: mark all N*v numbers (N - integer, N*v<=max) as non-prime
  static void MarkNonPrime3(CPrimeNumbers *sno, const uint v,const uint max); // Multithreading version: mark all N*v numbers (N - integer, N*v<=max) as non-prime
public:
  // Find all prime numbers - different realizations
  void Initiate1(const uint max=6); // Simple algorithm - checking each number
  void Initiate2(const uint max=6); // Marking-forward algorithm, single thread
  void Initiate3(const uint max=6); // Marking-forward algorithm, multithreading using C++11, std::thread
  void Initiate4(const uint max=6); // Marking-forward algorithm, multithreading using C++11, std::async
  void Initiate5(const uint max=6); // Marking-forward algorithm, multithreading using Boost
  void Initiate6(const uint max=6); // Marking-forward algorithm, multithreading using OpenMP
  
  void Print(); // Outhput prime numbers found
  bool IsPrime(uint number); // Check whether given number is prime
};

bool CPrimeNumbers::IsPrime(uint number)
{
  uint i;
  
  // BUG: check whether number is =0
  if (number<4)
    return true;
  
  if (number%2==0)
    return false;
  
  for (i=0;i<NumberToIndex(number);i++)
    if ( numbers[i]==1 && number%IndexToNumber(i) == 0 )
      return false;
    
  return true;
};

/*static*/ void CPrimeNumbers::MarkNonPrime3(CPrimeNumbers *sno, const uint v,const uint max)
{
  uint n;
  
  n=v*3;
  while (n<=max) {
    sno->numbers[sno->NumberToIndex(n)]=0;
    n+=v*2;
  }
};

void CPrimeNumbers::MarkNonPrime2(const uint v,const uint max)
{
  uint n;
  
  n=v*3;
  while (n<=max) {
    numbers[NumberToIndex(n)]=0;
    n+=v*2;
  }
};

void CPrimeNumbers::Initiate6(const uint max)
{
  uint head,i;
  
  numbers.assign(NumberToIndex(max)+1,1);
  
  for (head=3;head<=max;head+=2) {
    if ( numbers[NumberToIndex(head)]==1 ) { // Is prime/unchecked yet
      if (!IsPrime(head)) { // Is not prime
        numbers[NumberToIndex(head)]=0;
      }
      else{ // Is prime
        /*********** Multithreading using OpenMP ***********/
        #pragma omp parallel
        {
          MarkNonPrime3(this,head,max);
        }
      }
    }
  };
};

void CPrimeNumbers::Initiate5(const uint max)
{
  uint head,i;
  vector<boost::thread> threads;
  
  numbers.assign(NumberToIndex(max)+1,1);
  
  for (head=3;head<=max;head+=2) {
    if ( numbers[NumberToIndex(head)]==1 ) { // Is prime/unchecked yet
      if (!IsPrime(head)) { // Is not prime
        numbers[NumberToIndex(head)]=0;
      }
      else{ // Is prime
        /*********** Multithreading using Boost ***********/
        if ( threads.size()==MAX_THREAD) {
          threads[0].join();
          threads.erase(threads.begin());
        }
        threads.push_back( boost::thread(MarkNonPrime3,this,head,max) );
      }
    }
  };
  
  for (auto t=threads.begin();t!=threads.end();t++)
    t->join();
};

void CPrimeNumbers::Initiate4(const uint max)
{
  uint head,i;
  
  numbers.assign(NumberToIndex(max)+1,1);
  
  for (head=3;head<=max;head+=2) {
    if ( numbers[NumberToIndex(head)]==1 ) { // Is prime/unchecked yet
      if (!IsPrime(head)) { // Is not prime
        numbers[NumberToIndex(head)]=0;
      }
      else{ // Is prime
        /*********** multithreading using C++11, std::async ***********/
        async(launch::async,  MarkNonPrime3,this,head,max );
      }
    }
  };
};

void CPrimeNumbers::Initiate3(const uint max)
{
  uint head,i;
  vector<thread> threads;
  
  numbers.assign(NumberToIndex(max)+1,1);
  
  for (head=3;head<=max;head+=2) {
    if ( numbers[NumberToIndex(head)]==1 ) { // Is prime/unchecked yet
      if (!IsPrime(head)) { // Is not prime
        numbers[NumberToIndex(head)]=0;
      }
      else{ // Is prime
        /*********** Multithreading using C++11, std::thread ***********/
        if ( threads.size()==MAX_THREAD) {
          threads[0].join();
          threads.erase(threads.begin());
        }
        threads.push_back( thread(MarkNonPrime3,this,head,max) );
      }
    }
  };
  
  for (auto t=threads.begin();t!=threads.end();t++)
    t->join();
};

void CPrimeNumbers::Initiate2(const uint max)
{
  uint head,i;
  
  numbers.assign(NumberToIndex(max)+1,1);
  
  for (head=3;head<=max;head+=2) {
    if ( numbers[NumberToIndex(head)]==1 ) { // Is prime/unchecked yet
      if (!IsPrime(head)) { // Is not prime
        numbers[NumberToIndex(head)]=0;
      }
      else{ // Is prime
        /*********** Single thread ***********/
        MarkNonPrime2 (head,max);
      }
    }
  };
  
};

void CPrimeNumbers::Initiate1(const uint max)
{
  uint i;
  uint head;
  
  numbers.assign(NumberToIndex(max)+1,1);
  
  for (head=3;head<=max;head+=2) {
    if (! IsPrime(head) )
      numbers[NumberToIndex(head)]=0;
  };
  
};

void CPrimeNumbers::Print()
{
  uint n;
  
  n=3;
  for (auto i:numbers) {
    if (i) cout << n << " ";
    n+=2;
  }
  cout << endl;
}

int main()
{
  CPrimeNumbers s;
  
  /*********** Choose the algorithm ***********/
  s.Initiate1(100000);
  s.Print();
  
  return 0;
}

★★★★★

Последнее исправление: Kroz (всего исправлений: 5)

Ответ на: комментарий от AndreyKl

*И количество работы в функции IsSimple несравненно больше чем во всех остальных функциях твоей программы.

AndreyKl ★★★★★
()

кстати говоря, пролистал тред. вообще то проблему ещё на первой странице названа тут. буквально четвёртное сообщение в треде. quiet_readonly оказался достатчоно внимательным чтобы сразу заметить что ты IsSimple не вынес в потоки.

AndreyKl ★★★★★
()
Ответ на: комментарий от anonymous

$ time ./simple.3 >simple.3.out
$ time ./simple.3_2 >simple.3_2.out
подробности

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

Kroz ★★★★★
() автор топика
Ответ на: комментарий от anonymous

for(auto num = numbers.begin(); num != numbers.end() && i<*num; ++num, i+=2)

1. Нет. *num всегда ноль или единица (я сделал его unsigned char только для чистоты опыта, изначально там был bool), значит i<*num всегда false, значит IsPrime всегда будет возвращать true. Это не влияет когда MarkNonPrime будет исправно и (!)вовремя отрабатывать, а именно в Initiate2 (без многопоточности) или в многпоточных алгоритмах, при условии, что они будут выполняться быстрее основного потока. И будет приводить к проблеме когда MarkNonPrime нет (Initiate1) или когда какой-то поток запоздает (хотя во втором случае просто будет лишняя работа) . Уверен, что увеличение производительности только за счет де-факто отсутствия цикла в IsPrime.
2. Про то, что лучше юзать итераторы, знаю. Оптимизацией буду заниматься после того, как все заработает как нужно.

Kroz ★★★★★
() автор топика
Ответ на: комментарий от anonymous

тем не менее, по мере роста исследуемого числа IsPrime становится все сложнее вычислить

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

Kroz ★★★★★
() автор топика
Ответ на: комментарий от Kroz

в ряде сообщений упоминалось про valgrind, чем он вам не нравится? запускается так:

valgrind --tool=callgrind ./simple
callgrind_annotate callgrind.out.12345 >out.txt
первая команда создаст файл (callgrind.out.12345) для второй команды, где 12345 - это некий номер, pid процесса. после выполнения внимательно изучить файл out.txt в поисках «тяжелых» функций.

anonymous
()
Ответ на: комментарий от oami

Добавь в параметры компиляции -O2, будет побыстрее считать.

Знаю. Но основная тема топика: 1) откуда жесткое ограничение именно на одно ядро и 2) почему эффекта от многопоточности нет вообще? -O2 это не исправит.

не вижу смысла использовать прагму перед блоком, в котором стоит только вызов функции

Я на изучение OpenMP потратил 10 минут.
Как в OpenMP просто вызвать функцию в отдельном потоке?

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

Оптимизация - только после того как заработает как нужно.

Kroz ★★★★★
() автор топика
Ответ на: комментарий от Kroz

i<*num

на этот счет извиняюсь, был не прав. там должно быть i<number вероятно.

anonymous
()

а ты знаешь, что код в нитках и других вещах, который устанавливает numbers в единицу не имеет смысла в твоем коде из-за одной маленькой строчки:

numbers.assign(NumberToIndex(max)+1,1);

anonymous
()
Ответ на: комментарий от anonymous

следовательно единственное, что действительно нужно запускать в нитках это IsPrime, которая тяжелая, тебе это уже на 4-том сообщении сообщили.

anonymous
()
Ответ на: комментарий от anonymous

поторопился, опять не прав.

а ты знаешь, что код в нитках и других вещах, который устанавливает numbers в единицу не имеет смысла в твоем коде из-за одной маленькой строчки:
numbers.assign(NumberToIndex(max)+1,1);

anonymous
()
Ответ на: комментарий от Kroz

Уверен, что увеличение производительности только за счет де-факто отсутствия цикла в IsPrime.

это правда, i<*num, не верно было написано. результат работы был не верным.

anonymous
()
Ответ на: комментарий от Kroz

а как тебе тогда вот такой вариант?

void CPrimeNumbers::Initiate3_2(const uint max)
{
  uint head,i;
  vector<thread> threads;
  
  numbers.assign(NumberToIndex(max)+1,1);
  
  thread(MarkNonPrime3,this,3,max).join();
  for (head=5;head<=max;head+=2) {
    if ( threads.size()==MAX_THREAD) {
      threads[0].join();
      threads.erase(threads.begin());
    }
    if(numbers[NumberToIndex(head)]==1)
      threads.push_back( thread(MarkNonPrime3,this,head,max) );
  }
  
  for (auto t=threads.begin();t!=threads.end();t++)
    t->join();
};

anonymous
()
Ответ на: комментарий от anonymous

а как тебе тогда вот такой вариант?

Начинать с 5, а не с 3 (единственная разница, которую увидел)? Тогда MarkNonPrime не сработает для 3 и не пометит как не-простые 6, 9, 12, 15 ...

Остановимся пока на Мне всегда казалось, что несколько потоков должны грузить все ядра процессора... (комментарий) .

Спасибо за помощь.

Kroz ★★★★★
() автор топика
Ответ на: комментарий от AndreyKl

-I/home/myname/tmp/tbb41_20130314oss/includes

Я его даже скомплировал.

Пока ты лидируешь; твой ближайший конкурент выполняется на 8% дольше.
Подход забавный, возьму на вооружение. TBB, как я понял, только для Intel?

если тебе всё ещё интересно что там к чему, то вот фотка валгринда

valgrind - это, пожалуй, самое интересное, что я узнал из этого треда. Еще до конца не понял что это, но как будет время, обязательно погуглю. Мало решить проблему, нужно знать как решать подобные проблемы. А с многопоточностью не все так просто.

И количество работы не в функции IsSimple несравненно больше чем во всех остальных функциях твоей программы.

При маленьких числах основная работа по идее в MarkNonPrime. Особенно, при учете, что я не использую там итераторы. Ну хоть на начальном этапе, могла нагрузка распределиться по ядрам?

Ладно, закрываю тему.

Из полезного: valgrind, твой подход к многопоточности, ну, и в конце концов решенная конкретная задача.

Спа-си-бо!

Kroz ★★★★★
() автор топика
Ответ на: комментарий от Kroz

Начинать с 5, а не с 3 (единственная разница, которую увидел)?

а отсутствие функции IsPrime/IsSimple не аргумент?

anonymous
()
Ответ на: комментарий от Kroz

Тогда MarkNonPrime не сработает для 3 и не пометит как не-простые 6, 9, 12, 15 ...

а как же строчка:

thread(MarkNonPrime3,this,3,max).join();

anonymous
()
Ответ на: комментарий от Kroz

Ладно, закрываю тему.

тогда последний мой вариант в продолжение вот этой мысли. будет время собери.

#define MAX_THREAD 100
#define INTERVAL 1000

/*static*/ void CPrimeNumbers::MarkNonPrime3_2(CPrimeNumbers *sno, const uint head, const uint max)
{
  uint tail = head+INTERVAL;
  if(tail>max) tail = max;
  for(uint v=head; v<tail; v+=2)
    for(uint n=v*3;n<=max;n+=v*2)
      sno->numbers[sno->NumberToIndex(n)]=0;
};

void CPrimeNumbers::Initiate3_2(const uint max)
{
  uint head,i,t=0;
  vector<thread> threads;
  
  numbers.assign(NumberToIndex(max)+1,1);
  
  thread(MarkNonPrime3_2,this,3,max).join();
  t = (3+INTERVAL)*2;
  for (head=3+INTERVAL;head<=max;head+=2) {
    if ( (  threads.size()==MAX_THREAD                             ) || 
         ( (threads.size()!=0         ) && ( (head+INTERVAL) > t ) ) ) {
      t = (head-INTERVAL*threads.size()+INTERVAL)*2;
      threads[0].join();
      threads.erase(threads.begin());
    }
    if(numbers[NumberToIndex(head)]==1) {
      threads.push_back( thread(MarkNonPrime3_2,this,head,max) );
      head+=INTERVAL-2;
    }
  }
  
  for (auto t=threads.begin();t!=threads.end();t++)
    t->join();
};

anonymous
()
Ответ на: комментарий от anonymous

thread(MarkNonPrime3,this,3,max).join();

А, ну хорошо.
По по факту мы сэкономим пару тактов на вызов функции и почти одночастный возврат из нее. Кроме того, думаю, -O3 такое и само разрулит.

Kroz ★★★★★
() автор топика
Ответ на: комментарий от anonymous

а отсутствие функции IsPrime/IsSimple не аргумент?

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

Kroz ★★★★★
() автор топика
Ответ на: комментарий от Kroz

дело в отсутствии IsPrime, а эта строчка перестраховка на самом деле. в идеале, первые N раз такое сделать, а дальше можно потоки запускать. в последней версии программы это показано.

там же смотри, когда вычисляется число K является ли оно простым на его простоту не могут никак повлиять числа от K/2+1 до K-1, для этого первые несколько простых чисел нужно вычислить и поставить, а дальше можно либо надеяться, что потоки не успеют догнать границу «K/2», либо её контролировать и дожидаться нужных потоков.

надеяться - это первый вариант программы. контролировать - последний.

anonymous
()
Ответ на: комментарий от anonymous

Не запускал, но чую баг:

for (head=3+INTERVAL;head<=max;head+=2) {

И как оно доберется до, например 5?

for (head=3+INTERVAL;head<=max;head+=2) {

А если я задам max=10?

for (head=3+INTERVAL
head+=INTERVAL-2;
uint tail = head+INTERVAL;

Много добавлений, нигде не виду вычитаний. Очень быстро улетим за

;head<=max;

Kroz ★★★★★
() автор топика
Ответ на: комментарий от Kroz

И как оно доберется до, например 5?

вот так:

/*static*/ void CPrimeNumbers::MarkNonPrime3_2(CPrimeNumbers *sno, const uint head, const uint max) {
...
  for(uint v=head; v<tail; v+=2)
...
}
...
  thread(MarkNonPrime3_2,this,3,max).join();
...

anonymous
()
Ответ на: комментарий от Kroz

uint tail = head+INTERVAL;

тут вот ограничитель:

if(tail>max) tail = max;

А если я задам max=10?

можешь, ничего не случится, просто все сведется к одному потоку (при условии что INTERVAL>10)

anonymous
()

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

Для того чтобы убедиться, что твоя многопоточность даст ускорение, усложни задачу MarkNonPrime3. Например, так:

/*static*/ void CPrimeNumbers::MarkNonPrime3(CPrimeNumbers *sno, const uint v,const uint max)
{
  uint n;
  
  n=v*3;
  while (n<=max) {
    for (int i = 0; i < 10000; i++) { // делаем тысячи лишней работы, чисто чтобы позырить
	    sno->numbers[sno->NumberToIndex(n)]=0;
    }
    n+=v*2;
  }
};

Вот такой код уже нагружает все ядра и даёт ускорение. То есть ты просто изначально неправильно выбрал задачу, которую распараллеливаешь, и получил ускорение одного процента программы во много раз (то есть ничего не получил). Советую покурить «закон Амдала».

kvap
()
Ответ на: комментарий от anonymous

ещё по поводу границы, она на самом деле не K/2, а корень из K. есть такое определение: если натуральное число n>1 не делится ни на одно простое число, не превосходящее корня из n, то оно простое.

anonymous
()
Ответ на: комментарий от anonymous

да, ещё одна ошибка в функции CPrimeNumbers::MarkNonPrime3_2, нужно код:

  for(uint v=head; v<tail; v+=2)
    for(uint n=v*3;n<=max;n+=v*2)
заменить на вот такой код:
  for(uint v=head; v<tail; v+=2)
    if (sno->numbers[sno->NumberToIndex(v)]==1)
      for(uint n=v*3;n<=max;n+=v*2)
что-бы не выполнять лишней работы (не маркировать начиная с составного числа).

anonymous
()
Ответ на: комментарий от Kroz

ttb для intel/amd64. для x86(64) короче говоря.

При маленьких числах основная работа по идее в MarkNonPrime.

не знаю по какой идее. но основная работа у тебя в IsSimple. Погляди на фоточку валгринда.

Спа-си-бо!

рад был помочь). если хочешь ещё совет, то по-моему нужно в мою последнюю версию перенести оптимизации анонимуса. будет быстро и многопоточно. я то не оптимизировал.

AndreyKl ★★★★★
()
Ответ на: комментарий от Kroz

Как в OpenMP просто вызвать функцию в отдельном потоке?

OpenMP разрабатывался не для сложного управления потоками, а для простого распараллеливания. Для его применения желательно сразу выделять распараллеливаемые куски кода, поэтому вот эти слова:

Оптимизация - только после того как заработает как нужно.

означают, что OpenMP не для этого алгоритма (во всяком случае, в такой постановке). И та прагма, которая у тебя стоит, вряд ли что-то полезное вообще делает. Можно вынести ее перед циклом и сделать parallel for - но я не берусь предсказать корректность вычислений, упрятанных внутрь функций и потенциально обращающихся к одним и тем же данным.

oami ★★
()
Вы не можете добавлять комментарии в эту тему. Тема перемещена в архив.