Почему один процесс сортирует список намного быстрее, чем многие процессы сортируют отдельные списки одинакового размера? - PullRequest
0 голосов
/ 08 июня 2018

У меня 64 ядра на одной машине с сортировкой на 1 ГБ данных.Каждый из них сортирует 156 250 элементов и не должен совместно использовать какие-либо структуры данных (т. Е. Всего отсортировано 64 отдельных массива).Однако чем больше ядер у меня работает, тем медленнее каждое ядро ​​выполняет свою задачу сортировки.

Измерение времени выполняется следующим образом:

void sort_ranges(std::vector<std::vector<std::vector<int> > > & range_partitions, int num_workers, std::string filename, std::string outfile)
{
  #pragma omp parallel default(none) shared(range_partitions, outfile, num_workers)
  {
    int i = omp_get_thread_num();
    std::vector<int> data_vec; //Data copied into separate data structure for each thread
    for(int x = 0; x < num_workers; x ++) {
      data_vec.reserve(data_vec.size() + (range_partitions[x][i]).size());
      data_vec.insert(data_vec.end(), range_partitions[x][i].begin(), range_partitions[x][i].end());
    }
    int n = data_vec.size();
    int * data = &data_vec[0];
    double start = omp_get_wtime();
    std::sort(data, data + n); //Measure sort function call
    double sort_done = omp_get_wtime() - start;
  }
}

Когда я запускаю 1 ГБ данных, каждый процесс сортирует массив размером 156 250 и занимает около 10 секунд.Очевидно, это смехотворно медленно.Если я запускаю один процесс, который сортирует массив размером 156 250, процесс сортировки занимает <0,1 секунды.</p>

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

Я думаю, что что-то в том, как управляется память, мне не хватает.Любая помощь приветствуется!

Я понимаю, что для увеличения параллелизма очень много разных затрат, например, накладные расходы на процесс или работа с общей памятью, однако меня особенно беспокоит замедление std :: sort() функция вызывается для отдельных структур данных для каждого потока

Ответы [ 4 ]

0 голосов
/ 08 июня 2018

Ваш код исключает критическую заключительную скобку.

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

void sort_ranges(std::vector<std::vector<std::vector<int> > > & range_partitions, int num_workers, std::string filename, std::string outfile) {
  #pragma omp parallel default(none) shared(range_partitions, outfile, num_workers)
  {
    std::vector<int> data_vec; //Data copied into separate data structure for each thread
    for(int x = 0; x < num_workers; x ++) {
      data_vec.reserve(data_vec.size() + (range_partitions[x][i]).size());
      data_vec.insert(data_vec.end(), range_partitions[x][i].begin(), range_partitions[x][i].end());
    }
    int n        = data_vec.size();
    int * data   = &data_vec[0];
    double start = omp_get_wtime();
    std::sort(data, data + n); //Measure sort function call
    double sort_done = omp_get_wtime() - start;
  }
}

Я думаю, что ваш код не делает то, чтовы ожидаете.

#pragma omp parallel указывает, что каждый поток должен выполнять содержимое вашего блока.

Переменная i не появляется в вашем фрагменте кода, поэтому невозможно знать, что это делает.

Однако каждый поток, похоже, копирует несколько диапазонов в data_vec, после чего каждый поток сортирует одинаковые данные.

Вы можетехочу попробовать это вместо:

void sort_ranges(std::vector<std::vector<std::vector<int> > > & range_partitions, int num_workers, std::string filename, std::string outfile) {
  #pragma omp parallel for default(none) shared(range_partitions, outfile)
  for(int x=0;x<num_workers;x++){
    std::vector<int> data_vec(range_partitions[x][i].begin(), range_partitions[x][i].end()); //Data copied into separate data structure for each thread
    double start = omp_get_wtime();
    std::sort(data_vec.begin(), data_vec.end()); //Measure sort function call
    double sort_done = omp_get_wtime() - start;
  }
}
0 голосов
/ 08 июня 2018

Общая пропускная способность памяти ограничена, когда у вас есть данные больше, чем ваш кэш (и 1 ГБ данных, безусловно, выходит из вашего кэша) и плохой шаблон доступа (и сортировка, как правило, довольно плохо, особенно первые шаги) памятискорость будет вашим пределом.Если вы уже ограничиваете его одним ядром, параллельная сортировка N копий замедлит его в N раз - возможно, еще больше, поскольку вы также перебиваете кэш L3 (каждое ядро ​​пытается получить доступ к несвязанным данным).

0 голосов
/ 08 июня 2018

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

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

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

Cache hierarchy

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

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

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

perf stat -e cache-misses,cache-references,L1-dcache-load-misses,L1-dcache-loads,L1-dcache-stores,l2_rqsts.miss,LLC-load-misses,LLC-loads,LLC-prefetch-misses,LLC-store-misses,LLC-stores ./a.out m

Это приводит к следующему для однопоточной операции:

    18,676,838      cache-misses              #   69.492 % of all cache refs      (27.28%)
    26,876,349      cache-references                                              (36.38%)
   143,224,257      L1-dcache-load-misses     #    1.65% of all L1-dcache hits    (36.39%)
 8,682,532,168      L1-dcache-loads                                               (36.40%)
 4,130,005,905      L1-dcache-stores                                              (36.40%)
    92,709,572      l2_rqsts.miss                                                 (36.40%)
     2,409,977      LLC-load-misses           #   34.83% of all LL-cache hits     (36.39%)
     6,919,668      LLC-loads                                                     (36.37%)
    23,562,449      LLC-prefetch-misses                                           (18.16%)
    16,038,395      LLC-store-misses                                              (18.19%)
    79,580,399      LLC-stores                                                    (18.18%)

  24.578381342 seconds time elapsed

И для выполненияс четырьмя потоками:

    21,357,447      cache-misses              #   74.720 % of all cache refs      (23.99%)
    28,583,269      cache-references                                              (33.10%)
   160,265,596      L1-dcache-load-misses     #    1.85% of all L1-dcache hits    (35.91%)
 8,670,516,235      L1-dcache-loads                                               (36.52%)
 4,131,943,678      L1-dcache-stores                                              (36.50%)
   102,495,289      l2_rqsts.miss                                                 (36.50%)
     2,768,956      LLC-load-misses           #   38.05% of all LL-cache hits     (32.91%)
     7,277,568      LLC-loads                                                     (31.23%)
    29,220,858      LLC-prefetch-misses                                           (15.36%)
    18,920,533      LLC-store-misses                                              (15.26%)
   104,834,221      LLC-stores                                                    (14.85%)

  10.334248457 seconds time elapsed

Как видите, работа с четырьмя потоками привела к большему количеству промахов в кеше.Это не может быть статистически значимым увеличением;Я не запускался несколько раз, чтобы проверить.Однако, в отличие от вас, я вижу улучшение производительности благодаря большему количеству потоков.

Чтобы имитировать конкуренцию в кеше, я могу переподписать свой ЦП, используя больше потоков, чем ядер.Для этого я установил переменную окружения OMP_NUM_THREADS:

export OMP_NUM_THREADS=32

С 32 потоками, я вижу:

Статистика счетчика производительности для './a.out m':

    24,222,105      cache-misses              #   77.175 % of all cache refs      (23.39%)
    31,385,891      cache-references                                              (32.47%)
   161,353,805      L1-dcache-load-misses     #    1.87% of all L1-dcache hits    (35.27%)
 8,618,074,931      L1-dcache-loads                                               (36.70%)
 4,131,633,620      L1-dcache-stores                                              (36.28%)
   107,094,632      l2_rqsts.miss                                                 (36.21%)
     5,299,670      LLC-load-misses           #   56.36% of all LL-cache hits     (31.93%)
     9,403,090      LLC-loads                                                     (29.02%)
    46,500,188      LLC-prefetch-misses                                           (15.09%)
    20,131,861      LLC-store-misses                                              (14.26%)
   105,310,438      LLC-stores                                                    (14.15%)

  10.379022550 seconds time elapsed

Обратите внимание, что количество пропущенных загрузок LLC (кэш последнего уровня) увеличилось с 34% до 38% до 56% с увеличением количества потоков.Скорость, однако, не сильно пострадала.Это может быть из-за того, что у данных нет хорошего расположения кеша.

В любом случае, это один из способов изучения вашей проблемы.Если вам нужна лучшая помощь, чем эта, вам придется создать собственный MWE.

Вы можете уменьшить некоторые конфликты в кеше, уменьшив количество используемых вами потоков и указав их сходство, чтобы потокине используйте одни и те же кэши L2 / L3 (в зависимости от вашего процессора).Более подробная информация здесь .

Минимальный рабочий пример

#include <algorithm>
#include <chrono>
#include <iostream>
#include <random>
#include <vector>

typedef std::vector< std::vector<int> > data_t;

data_t GenData(std::mt19937 &mt_rand, int vec_num, int vec_len){
  data_t data;
  data.reserve(vec_num);
  for(unsigned int i=0;i<vec_num;i++){
    data.emplace_back();
    data.back().reserve(vec_len);
    for(unsigned int i=0;i<vec_len;i++)
      data.back().emplace_back(mt_rand());
  }
  return data;
}

void SortSingle(data_t &data){
  for(auto &v: data)
    std::sort(v.begin(),v.end());
}

void SortMulti(data_t &data){
  #pragma omp parallel for default(none) shared(data)
  for(unsigned int i=0;i<data.size();i++)
    std::sort(data[i].begin(), data[i].end());
}

int main(int argc, char **argv){
  std::mt19937 mt_rand;

  typedef std::chrono::high_resolution_clock clock;

  std::cout<<"Generating data..."<<std::endl;
  auto data = GenData(mt_rand,1600,156250);

  std::cout<<"Sorting data..."<<std::endl;
  const auto start_time = clock::now();
  if(argv[1][0]=='s')
    SortSingle(data);
  else if (argv[1][0]=='m')
    SortMulti(data);
  else
    std::cout<<"Unknown sort type!"<<std::endl;
  const auto end_time = clock::now();

  const auto time_diff = std::chrono::duration_cast<std::chrono::duration<double>>(end_time - start_time).count();

  std::cout<<"Time = "<<time_diff<<"s"<<std::endl;

  return 0;
}
0 голосов
/ 08 июня 2018

При параллельном программировании необходимо учитывать ряд факторов.

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

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

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

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

Это лишь некоторые из факторов, которые следует учитывать.

...