为什么这个包含rand()的C++ 11代码使用多个线程而不是一个?

Bas*_*sti 41 c++ performance multithreading c++11

我正在尝试新的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;
}
Run Code Online (Sandbox Code Playgroud)

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

time single: 33
time multi: 565
Run Code Online (Sandbox Code Playgroud)

因此多线程版本的速度要慢一个数量级.我使用了随机数和一个sqrt来使这个例子变得不那么简单并且容易进行编译器优化,所以我没有想法.

编辑:

  1. 这个问题可以扩展为更大的N,因此问题不在于运行时间短
  2. 创建线程的时间不是问题.排除它不会显着改变结果

哇我发现了这个问题.这确实是兰德().我用C++ 11等价替换它,现在运行时完美地扩展.感谢大家!

小智 26

在我的系统上,行为是相同的,但正如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;
}
Run Code Online (Sandbox Code Playgroud)

  • 在我看来,问题实际上是`rand`**是**线程安全的,并且当多个线程都调用`rand`时存在大量的锁争用.使用`rand_r`,每个调用都有自己的数据,因此没有争用. (9认同)
  • "**函数rand()不是**可重入或**线程安全**"并不意味着rand不是线程安全的? (4认同)
  • @PeteBecker你可能*你的`rand`的特定实现选择是线程安全的(由时序数据证明).但是如果文档明确表示它不是线程安全的,那么依赖它就是一个坏主意 - 未来版本可以在没有警告的情况下自由更改,并以非常难以调试的方式破坏您的代码. (2认同)

Nat*_*ohl 20

正如你所发现的,这rand是罪魁祸首.

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

例如,eglibc定义rand__random,定义为:

long int
__random ()
{
  int32_t retval;

  __libc_lock_lock (lock);

  (void) __random_r (&unsafe_state, &retval);

  __libc_lock_unlock (lock);

  return retval;
}
Run Code Online (Sandbox Code Playgroud)

这种锁定会迫使多个线程串行运行,从而导致性能降低.


Cla*_*dio 8

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