为什么单个进程对列表进行排序的速度比让许多进程对单独大小的单独列表进行排序要快得多?

时间:2018-06-08 17:13:01

标签: c++ parallel-processing multiprocessing openmp multicore

我在一台计算机上运行64个内核,对总共1GB的数据进行排序。它们每个排序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;
  }
}

当我运行1GB的数据时,每个进程对一个156,250的数组进行排序,大约需要10秒。显然这是非常缓慢的。如果我运行一个对156,250大小进行排序的进程,则该进程需要&lt; 0.1秒排序。

我真的对此感到困惑,因为每个进程都在不同的阵列上运行,所以没有理由让更多核心运行相同的任务会减慢所有其他核心的速度。

我认为有一些关于我如何管理内存的问题。任何帮助表示赞赏!

我意识到增加并行性有很多不同的成本,例如:处理开销或处理共享内存,但是我特别关注为每个线程在单独的数据结构上调用的std :: sort()函数的减速

4 个答案:

答案 0 :(得分:6)

当您的数据大于缓存(并且1 GB的数据肯定会从缓存中移出)和错误的访问模式(并且排序通常非常糟糕,尤其是第一步)内存时,总内存带宽是有限的速度将是你的极限。如果你已经用一个核心来限制它,那么并行排序它的N个副本会减慢它的速度N次 - 可能更多,因为你还在颠覆L3缓存(每个核心都试图访问不相关的数据)。

答案 1 :(得分:3)

您没有在问题中加入minimum working example,因此我无法重现您的问题。

我同意其他人的意见,你可能会看到的是,在cache thrashing中使用太多内核来进行排序会产生结果,尽管我还没有能够根据我的证据来证明这一点自己的测试。

当CPU从内存中读取数据时,它不会读取一个字节。它读取许多字节。它们存储在缓存中以便快速访问。高速缓存是分层的,并且在处理器之间或多或少地共享,如下所示:

Cache hierarchy

如您所见,核心都共享L3缓存。如果内核运行的内存地址彼此远离,则内核将具有有限的缓存重叠并竞争利用缓存。

验证代码中是否发生这种情况很容易(至少,如果你有Linux)。您可以使用perf command收集有关您的计划正在执行的操作的数据。

在这个问题的底部,我列出了我认为你所询问的MWE。然后,我使用以下perf命令收集有关MWE行为的统计信息。

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

正如您所看到的,使用四个线程运行确实导致更多的缓存未命中 我。这可能不是一个统计上显着的增长;我没有多次运行 时间检查。但是,与您不同,我看到更多的性能提升 线程。

为了模拟缓存争用,我可以通过使用比核心更多的线程来超额预订我的CPU。为此,我设置了OMP_NUM_THREADS环境变量:

export OMP_NUM_THREADS=32

有32个线程,我看到了:

&#39; ./ a.out m&#39;的性能计数器统计信息:

    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-load-miss(最后一级缓存)已从34%上升到38% 线程数增加56%。然而,速度并没有太大影响。 这可能是因为数据开始时没有良好的缓存局部性。

无论如何,这是研究问题的一种方法。如果你想要更好的帮助 这个,你必须自己做一个MWE。

您可以通过减少正在使用的线程数并指定其关联性来缓解某些缓存争用,以便线程不共享相同的L2 / L3缓存(取决于您的处理器)。更多信息是here

最低工作范例

#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;
}

答案 2 :(得分:2)

你的代码留下了一个关键的最终支撑。

我认为您打算编写的代码如下所示。

$(this).text().length

我认为您的代码没有达到预期效果。

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

答案 3 :(得分:0)

使用并行编程时,必须考虑许多因素。

首先,您具有创建单独线程/设置多个进程的不可忽略的启动成本(开销)。出于这个原因,添加并行性通常会使事情更少有效运行,除非您有足够的数据运行多个线程实际上会改善整个运行时。

其次,必须将这些任务安排到您可用的核心数量上。如果您有4个核心和64个任务,则需要将这64个任务安排到核心上 - 如果每个任务需要不同的时间来完成,这是一项非常重要的任务。

第三,如果线程之间存在任何干扰,那么这可能会减慢速度,特别是对于大量线程。

此外,存在偏斜方面,其中最慢的任务是瓶颈 - 直到最慢的任务完成,整个过程集不被视为已完成。

这些只是应该考虑的一些因素。