【发布时间】:2019-03-10 01:53:25
【问题描述】:
我目前正在研究一个科学模拟(Gravitational nbody)。我首先用一个简单的单线程算法编写了它,这对于少数粒子来说是可以接受的。然后我对这个算法进行了多线程处理(它是令人尴尬的并行),程序花费了大约 3 倍的时间。下面是一个最小的、完整的、可验证的简单算法示例,它具有相似的属性并输出到 /tmp 中的文件(它被设计为在 Linux 上运行,但 C++ 也是标准的)。请注意,如果您决定运行此代码,它将生成一个 152.62MB 的文件。输出数据是为了防止编译器优化程序外的计算。
#include <iostream>
#include <functional>
#include <thread>
#include <vector>
#include <atomic>
#include <random>
#include <fstream>
#include <chrono>
constexpr unsigned ITERATION_COUNT = 2000;
constexpr unsigned NUMBER_COUNT = 10000;
void runThreaded(unsigned count, unsigned batchSize, std::function<void(unsigned)> callback){
unsigned threadCount = std::thread::hardware_concurrency();
std::vector<std::thread> threads;
threads.reserve(threadCount);
std::atomic<unsigned> currentIndex(0);
for(unsigned i=0;i<threadCount;++i){
threads.emplace_back([¤tIndex, batchSize, count, callback]{
unsigned startAt = currentIndex.fetch_add(batchSize);
if(startAt >= count){
return;
}else{
for(unsigned i=0;i<count;++i){
unsigned index = startAt+i;
if(index >= count){
return;
}
callback(index);
}
}
});
}
for(std::thread &thread : threads){
thread.join();
}
}
void threadedTest(){
std::mt19937_64 rnd(0);
std::vector<double> numbers;
numbers.reserve(NUMBER_COUNT);
for(unsigned i=0;i<NUMBER_COUNT;++i){
numbers.push_back(rnd());
}
std::vector<double> newNumbers = numbers;
std::ofstream fout("/tmp/test-data.bin");
for(unsigned i=0;i<ITERATION_COUNT;++i) {
std::cout << "Iteration: " << i << "/" << ITERATION_COUNT << std::endl;
runThreaded(NUMBER_COUNT, 100, [&numbers, &newNumbers](unsigned x){
double total = 0;
for(unsigned y=0;y<NUMBER_COUNT;++y){
total += numbers[y]*(y-x)*(y-x);
}
newNumbers[x] = total;
});
fout.write(reinterpret_cast<char*>(newNumbers.data()), newNumbers.size()*sizeof(double));
std::swap(numbers, newNumbers);
}
}
void unThreadedTest(){
std::mt19937_64 rnd(0);
std::vector<double> numbers;
numbers.reserve(NUMBER_COUNT);
for(unsigned i=0;i<NUMBER_COUNT;++i){
numbers.push_back(rnd());
}
std::vector<double> newNumbers = numbers;
std::ofstream fout("/tmp/test-data.bin");
for(unsigned i=0;i<ITERATION_COUNT;++i){
std::cout << "Iteration: " << i << "/" << ITERATION_COUNT << std::endl;
for(unsigned x=0;x<NUMBER_COUNT;++x){
double total = 0;
for(unsigned y=0;y<NUMBER_COUNT;++y){
total += numbers[y]*(y-x)*(y-x);
}
newNumbers[x] = total;
}
fout.write(reinterpret_cast<char*>(newNumbers.data()), newNumbers.size()*sizeof(double));
std::swap(numbers, newNumbers);
}
}
int main(int argc, char *argv[]) {
if(argv[1][0] == 't'){
threadedTest();
}else{
unThreadedTest();
}
return 0;
}
当我运行它(在 Linux 上使用 clang 7.0.1 编译)时,我从 Linux time 命令得到以下时间。这些之间的区别与我在真实程序中看到的相似。标记为“真实”的条目与此问题相关,因为这是程序运行所需的时钟时间。
单线程:
real 6m27.261s
user 6m27.081s
sys 0m0.051s
多线程:
real 14m32.856s
user 216m58.063s
sys 0m4.492s
因此,当我预计它会显着加速(大约是 8 倍,因为我有一个 8 核 16 线程 CPU)时,我会问是什么导致了这种大幅减速。我没有在 GPU 上实现这一点,因为下一步是对算法进行一些更改,以将其从 O(n²) 变为 O(nlogn),但这对 GPU 也不友好。与包含的示例相比,更改后的算法与我当前实现的 O(n²) 算法的差异较小。最后,我想观察到运行每次迭代的主观时间(根据出现的迭代行之间的时间来判断)在线程和非线程运行中都发生了显着变化。
【问题讨论】:
-
如果您的线程使用共享数据和原子变量或锁,那么所有同步成本很可能完全抵消了并行化带来的任何好处。为了获得最佳性能,不共享任何内容,或者尽可能少共享。你能用 map/reduce 风格的策略来解决这个问题吗?
-
@tadman 正如我所见,原子变量每个线程只能访问一次。如果有 8 个线程,它只会被访问 8 次,这是非常少的。
-
这正是我所期望的,但肯定还有其他阻碍性能的因素。你为什么使用
constexpr来表示简单的数字?为什么不只是const?我无法重现您的问题,因为运行此代码时出现段错误。 -
你确定你正确地分配了工作吗?我可以看到偏移调整,但它们似乎都在迭代
size条目,因此由于重复工作,在您的线程版本中它应该会慢很多。
标签: c++ multithreading performance simd