我在一台计算机上运行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()函数的减速
答案 0 :(得分:6)
当您的数据大于缓存(并且1 GB的数据肯定会从缓存中移出)和错误的访问模式(并且排序通常非常糟糕,尤其是第一步)内存时,总内存带宽是有限的速度将是你的极限。如果你已经用一个核心来限制它,那么并行排序它的N个副本会减慢它的速度N次 - 可能更多,因为你还在颠覆L3缓存(每个核心都试图访问不相关的数据)。
答案 1 :(得分:3)
您没有在问题中加入minimum working example,因此我无法重现您的问题。
我同意其他人的意见,你可能会看到的是,在cache thrashing中使用太多内核来进行排序会产生结果,尽管我还没有能够根据我的证据来证明这一点自己的测试。
当CPU从内存中读取数据时,它不会读取一个字节。它读取许多字节。它们存储在缓存中以便快速访问。高速缓存是分层的,并且在处理器之间或多或少地共享,如下所示:
如您所见,核心都共享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个任务安排到核心上 - 如果每个任务需要不同的时间来完成,这是一项非常重要的任务。
第三,如果线程之间存在任何干扰,那么这可能会减慢速度,特别是对于大量线程。
此外,存在偏斜方面,其中最慢的任务是瓶颈 - 直到最慢的任务完成,整个过程集不被视为已完成。
这些只是应该考虑的一些因素。