为什么这个包含rand()的C ++ 11代码比使用multithreading更慢?

我正在尝试新的C ++ 11线程,但是我的简单testing具有糟糕的多核性能。 作为一个简单的例子,这个程序加起来一些平方随机数。

#include <iostream> #include <thread> #include <vector> #include <cstdlib> #include <chrono> #include <cmath> double add_single(int N) { double sum=0; for (int i = 0; i < N; ++i){ sum+= sqrt(1.0*rand()/RAND_MAX); } return sum/N; } void add_multi(int N, double& result) { double sum=0; for (int i = 0; i < N; ++i){ sum+= sqrt(1.0*rand()/RAND_MAX); } result = sum/N; } int main() { srand (time(NULL)); int N = 1000000; // single-threaded auto t1 = std::chrono::high_resolution_clock::now(); double result1 = add_single(N); auto t2 = std::chrono::high_resolution_clock::now(); auto time_elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(t2-t1).count(); std::cout << "time single: " << time_elapsed << std::endl; // multi-threaded std::vector<std::thread> th; int nr_threads = 3; double partual_results[] = {0,0,0}; t1 = std::chrono::high_resolution_clock::now(); for (int i = 0; i < nr_threads; ++i) th.push_back(std::thread(add_multi, N/nr_threads, std::ref(partual_results[i]) )); for(auto &a : th) a.join(); double result_multicore = 0; for(double result:partual_results) result_multicore += result; result_multicore /= nr_threads; t2 = std::chrono::high_resolution_clock::now(); time_elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(t2-t1).count(); std::cout << "time multi: " << time_elapsed << std::endl; return 0; } 

在Linux和3核机器上用'g ++ -std = c ++ 11 -pthread test.cpp'编译,一个典型的结果是

 time single: 33 time multi: 565 

所以multithreading版本的速度要慢一个数量级以上。 我使用了随机数和sqrt来使得这个例子不那么简单,而且很容易编译器优化,所以我没有想法。

编辑

  1. 这个问题规模较大的N,所以问题不是短的运行时间
  2. 创build线程的时间不是问题。 排除它并不会显着改变结果

哇,我发现这个问题。 这确实是兰德()。 我用一个C ++ 11replace它,现在运行时间完美地缩放。 感谢大家!

在我的系统上,行为是一样的,但正如Maxim所说的,rand不是线程安全的。 当我将rand改为rand_r时,multithreading代码如预期的那样更快。

 void add_multi(int N, double& result) { double sum=0; unsigned int seed = time(NULL); for (int i = 0; i < N; ++i){ sum+= sqrt(1.0*rand_r(&seed)/RAND_MAX); } result = sum/N; } 

正如你发现的, rand是这里的罪魁祸首。

对于那些好奇的人来说,这种行为可能来自于你为了线程安全而使用互斥锁来实现rand

例如, eglibc根据__random定义rand , 定义如下 :

 long int __random () { int32_t retval; __libc_lock_lock (lock); (void) __random_r (&unsafe_state, &retval); __libc_lock_unlock (lock); return retval; } 

这种locking会强制多个线程串行运行,从而导致性能下降。

执行程序所需的时间非常短(33毫秒)。 这意味着创build和处理多个线程的开销可能不仅仅是真正的好处。 尝试使用需要更长时间执行的程序(例如,10秒)。

为了加快速度,请使用线程池模式。

这将让你排队在其他线程的任务,而无需每次你想使用多个std::thread创build一个std::thread的开销。

不要指望在性能指标中设置队列的开销,只是排队和提取结果的时间。

创build一组线程和一个任务队列(一个包含std::function<void()>来提供它们。 线程在队列中等待新任务执行,执行它们,然后等待新任务。

这些任务负责将他们的“完成”通知给调用上下文,比如通过std::future<> 。 可以让你将函数排入任务队列的代码可以为你做这个包装,也就是这个签名:

 template<typename R=void> std::future<R> enqueue( std::function<R()> f ) { std::packaged_task<R()> task(f); std::future<R> retval = task.get_future(); this->add_to_queue( std::move( task ) ); // if we had move semantics, could be easier return retval; } 

它将一个裸的std::function返回给一个空的packaged_task ,然后将其添加到任务队列中。 请注意,任务队列需要移动感知,因为packaged_task是仅移动的。

注1:我不是那么熟悉std::future ,所以上面的代码可能是错误的。

注2:如果放入上述队列的任务相互依赖中间结果,则队列可能会死锁,因为没有提供“回收”被阻塞的线程并执行新代码的描述。 然而,“裸计算”非阻塞任务应该与上述模型正常工作。