C++ 为什么是C++;11包含rand()的代码在多个线程中比在一个线程中慢?

C++ 为什么是C++;11包含rand()的代码在多个线程中比在一个线程中慢?,c++,multithreading,performance,c++11,C++,Multithreading,Performance,C++11,我正在尝试新的C++11线程,但我的简单测试具有糟糕的多核性能。作为一个简单的例子,这个程序将一些平方随机数相加 #include <iostream> #include <thread> #include <vector> #include <cstdlib> #include <chrono> #include <cmath> double add_single(int N) { double sum=0;

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

#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;
}
因此,多线程版本要慢一个数量级以上。我使用了随机数和sqrt使示例变得不那么琐碎,并且易于进行编译器优化,所以我没有主意了

编辑

  • 这个问题会扩展到更大的N,因此问题不是短期的运行时间
  • 创建线程的时间不是问题所在。排除它不会显著改变结果

  • 哇,我发现了问题。确实是兰德()。我用C++11等价物替换了它,现在运行时可以完美地扩展。谢谢大家

    执行程序所需的时间非常短(33毫秒)。这意味着创建和处理多个线程的开销可能会超过实际的好处。尝试使用需要更长执行时间(例如10秒)的程序。

    要加快执行速度,请使用线程池模式

    这将允许您将任务排队到其他线程中,而无需在每次要使用多个线程时创建
    std::thread

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

    创建一组线程和一个任务队列(一个包含
    std::function
    的结构)来为它们提供数据。线程在队列中等待新任务执行,执行它们,然后等待新任务

    任务负责将其“已完成”信息传递回调用上下文,例如通过
    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::future排队(std::函数f){
    std::打包任务(f);
    std::future retval=task.get_future();
    这->将_添加到_队列(std::move(task));//如果我们有移动语义,可能会更容易
    返回返回;
    }
    
    它将一个返回
    R
    的裸
    std::函数
    转换为一个空的
    打包任务
    ,然后将其添加到任务队列中。请注意,任务队列需要具有移动意识,因为
    打包的_任务
    仅为移动

    注1:我对std::future不太熟悉,因此上述内容可能有误


    注2:如果放入上述队列的任务在中间结果方面相互依赖,则队列可能会死锁,因为没有描述“回收”被阻止的线程并执行新代码的规定。但是,“裸计算”非阻塞任务应该可以很好地使用上述模型。

    在我的系统上,行为是相同的,但正如Maxim提到的,rand不是线程安全的。当我将rand改为rand_r时,多线程代码会像预期的那样更快

    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;
    }
    
    void add_multi(int N、double和result){
    双和=0;
    无符号整数种子=时间(NULL);
    对于(int i=0;i
    正如您所发现的,是这里的罪魁祸首

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

    例如,根据
    \uu random
    定义
    rand
    ,其中:


    这种锁定将强制多个线程串行运行,从而导致性能降低。

    您正在测量创建线程的算法+时间,由于系统调用,创建线程的速度很慢。创建线程后移动计时器,然后运行线程。
    rand()
    通常不是多线程安全功能。使用
    rand\u r()
    +1来回答结构良好的问题。非常感谢您的sscce和编译器指令。请扩展@MM.的注释:500ms的意义不足以说明问题,线程创建时间远远超过您的算法运行时间。以更高的
    N
    (例如
    N=100000000
    )运行它,并给出结果@MaximYegorushkin:更好的方法是使用C++11随机引擎,比如
    std::minstd\u rand
    。他只创建了3个线程。这并不能解释565ms。我无法在VS2012上重现结果,因此我怀疑这里有其他问题。正如编辑中所述,问题扩大了。在我的Linux系统上,使用g++4.7和-O3时,我的结果是相同或可比的。这是一个建议,并没有回答实际问题。您可以用
    打包的任务
    替换
    共享的ptr
    和lambda表达式,这将使
    排队
    更容易simpler@JonathanWakely我认为是这样的。在我看来,问题实际上是
    rand
    是线程安全的,当多个线程都调用
    rand
    时,存在大量的锁争用。使用
    rand_r
    时,每个调用都有自己的数据,因此没有争用。@PeteBecker我也像你一样认为,但是
    rand
    手册页状态
    函数rand()不是可重入的或线程安全的,因为它使用在每次调用时修改的隐藏状态。
    @Étienne-使用隐藏状态意味着它不是可重入的。这并不意味着它不是线程安全的。如果将
    rand
    更改为
    rand\u r
    会使其速度更快,那么这就相当于确定
    rand
    正在同步其内部状态。“函数rand()不可重入或线程安全”这不意味着rand不是线程安全的吗?@PeteBecker您的
    rand
    的特定实现可能选择线程安全(如计时数据所示)。但是如果文档明确指出它不是线程安全的,那么依赖它是一个坏主意——未来的版本可以在没有警告的情况下自由更改,并以非常难以调试的方式破坏代码。
    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;
    }
    
    long int
    __random ()
    {
      int32_t retval;
    
      __libc_lock_lock (lock);
    
      (void) __random_r (&unsafe_state, &retval);
    
      __libc_lock_unlock (lock);
    
      return retval;
    }